Skip to content

feat(rate-limit): key by authKey hash, not just IP#65

Merged
CryptoJones merged 1 commit into
masterfrom
feat/rate-limit-per-authkey
May 18, 2026
Merged

feat(rate-limit): key by authKey hash, not just IP#65
CryptoJones merged 1 commit into
masterfrom
feat/rate-limit-per-authkey

Conversation

@CryptoJones
Copy link
Copy Markdown
Owner

Summary

express-rate-limit's default IP keying causes shared-IP false positives (mobile carrier NAT, corp egress) and is easy to dodge by rotating source IPs. Switching to:

  • authKey present → key by k:<sha256-prefix-of-authKey> (deterministic, separates budgets per credential)
  • authKey absent → IP fallback (the brute-force path; per-IP is correct there)

Extracted into app/middleware/rate-limit-key.js for testability.

Test plan

  • 7 unit tests: deterministic mapping, distinct keys → distinct budgets, IP fallback path, raw authKey never leaks into the key
  • Suite: 241 passing + 4 integration skipped (was 234 + 4 skipped)
  • Live load test (out of scope here)

Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/

express-rate-limit defaults to keying by IP, which has two
problems against an authenticated API:

  1. Shared-IP false positives. Mobile-carrier NAT, corporate
     egress, a household behind one router, k8s pods sharing a
     SNAT — all of these put legitimate clients on a shared
     budget. A noisy neighbor poisons everyone else.

  2. Brute-force pivot. An attacker rotating source IPs (vps
     pool, residential proxy, tor) stretches a per-IP budget
     by however many addresses they can spin up.

The new key generator looks at the `authKey` header:
  - present → key by `k:<sha256-prefix-of-authKey>`
  - absent  → key by `ip:<remote_address>` (the brute-force path)

Authenticated clients get their own per-key budget regardless
of egress IP. Anonymous requests (someone fishing for a valid
key) still get per-IP keying — the right granularity there.

Extracted into app/middleware/rate-limit-key.js so the keying
logic is unit-testable. server.js just imports it.

Tests: 7 cases in tests/unit/rate-limit-key.test.js — verifies
deterministic mapping, separation between distinct authKeys, IP
fallback, and that the raw authKey never appears in the
returned key (in case the rate-limiter store becomes redis-
backed later).

Suite: 33 files / 241 passing + 4 integration skipped (was 32 / 234).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@CryptoJones CryptoJones merged commit 3d9263a into master May 18, 2026
2 of 3 checks passed
@CryptoJones CryptoJones deleted the feat/rate-limit-per-authkey branch May 18, 2026 02:04
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.

1 participant