Skip to content

fix(auth): key rate-limit bucket by key_id/hash, never plaintext API key (S-6)#209

Merged
brownjuly2003-code merged 1 commit into
mainfrom
fix/ratelimit-key-no-plaintext
Jul 19, 2026
Merged

fix(auth): key rate-limit bucket by key_id/hash, never plaintext API key (S-6)#209
brownjuly2003-code merged 1 commit into
mainfrom
fix/ratelimit-key-no-plaintext

Conversation

@brownjuly2003-code

Copy link
Copy Markdown
Owner

What

_rate_limit_key returned the plaintext API key, which rate_limiter.check() uses verbatim as a Redis sorted-set key name (zadd(key, ...), expire(key, ...)). Anyone with Redis read access — a compromise, SCAN/MONITOR, a backup, a shared cache — could recover live plaintext API keys straight from the bucket names (audit S-6, P3, secret-at-rest disclosure).

How

Key the bucket by a stable non-secret identifier instead:

  1. key_id (stable, non-secret) →
  2. stored key_hash
  3. SHA-256 of the plaintext (never the plaintext itself) →
  4. non-secret name (last resort).

Each distinct key still maps to its own bucket, so per-key rate-limit isolation is unchanged; only a one-time bucket-namespace reset happens on deploy (60s windows, expire immediately). No migration needed.

Tests

  • Two prior tests encoded the vulnerable contract (asserted bucket == plaintext) — rewritten to assert the secure contract.
  • New coverage: the bucket never contains the plaintext; key_id is preferred over secret material; the key is deterministic and per-key distinct (isolation preserved).
  • Local: 89 (test_auth_manager_mutation) + 62 (broader auth/rate-limit suites) pass; ruff check + format + mypy clean.

🤖 Generated with Claude Code

…key (S-6)

`_rate_limit_key` returned the plaintext API key, which the rate limiter uses
verbatim as a Redis sorted-set key NAME (`zadd(key, ...)`). Anyone with Redis
read access (SCAN/MONITOR/a backup/a shared cache) could recover live plaintext
API keys straight from the bucket names (audit S-6, P3, secret-at-rest).

Key the bucket by the stable non-secret `key_id`, then the stored `key_hash`,
then a SHA-256 of the plaintext (never the plaintext itself), finally the
non-secret name. Each distinct key still maps to its own bucket, so per-key
rate-limit isolation is unchanged. Two prior tests encoded the plaintext-keying
contract (asserted the bucket == the plaintext); rewritten to assert the secure
contract, plus new coverage that the bucket never contains the plaintext.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

DORA Metrics

  • Window: last 30 days
  • Branch: main
  • Deployment frequency: 106 total / 24.73 per week
  • Lead time for changes: avg 0.61h / median 0.0h
  • Change failure rate: 75.47% (80/106)
  • MTTR: 20.13h across 5 incident(s)

@brownjuly2003-code
brownjuly2003-code merged commit 328754e into main Jul 19, 2026
26 checks passed
@brownjuly2003-code
brownjuly2003-code deleted the fix/ratelimit-key-no-plaintext branch July 19, 2026 12:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants