Skip to content

fix(rate-limit): share counters via Redis store and drop spoofable client IPs - #22

Open
naobadiah01 wants to merge 1 commit into
Kqirox:mainfrom
naobadiah01:fix/issue-12-rate-limit-redis
Open

fix(rate-limit): share counters via Redis store and drop spoofable client IPs#22
naobadiah01 wants to merge 1 commit into
Kqirox:mainfrom
naobadiah01:fix/issue-12-rate-limit-redis

Conversation

@naobadiah01

Copy link
Copy Markdown

Summary

Closes #12. Rate limiting was a per-process Map keyed by a client-supplied
header — counters reset on every restart, were per-replica, and the identity
could be rotated by sending a different x-forwarded-for per request.

Changes

  • Shared atomic store (src/middleware/rate-limit-store.ts): new
    RateLimitStore interface with an atomic increment(key, windowMs)
    returning { count, resetTime }. A RedisStore implements it with a Lua
    script (INCR + PEXPIRE, fixed-window), and InMemoryStore remains as
    the fallback for development and tests.
  • One store for all limiters: generalLimiter, authLimiter,
    employerLimiter, and authenticatedLimiter all share the single store
    singleton, so limits are combined across replicas and survive restarts
    when REDIS_URL is configured.
  • Non-spoofable identity: getClientIP uses the socket address unless
    TRUST_PROXY=true is set; only then is the rightmost x-forwarded-for
    hop trusted. Clients can no longer rotate headers to reset their budget.
  • Fail-open: if the store is unreachable the request proceeds rather
    than blocking valid traffic.
  • Docs/env: REDIS_URL and TRUST_PROXY added to .env.example,
    docs/ARCHITECTURE.md, and docs/OPERATIONS.md.

Acceptance criteria

  • Single shared store backs all four limiters; counters survive restart
  • Two processes sharing the store enforce a combined limit
  • Trusted-proxy mode keys on the rightmost trusted hop
  • Without a trusted proxy, client-supplied x-forwarded-for cannot rotate the key
  • Unit tests cover fixed-window reset, 429 + headers, and identity behavior
  • In-memory store retained as test fallback
  • Docs and .env.example document the Redis requirement and proxy setting

Tests

pnpm lint, pnpm test:coverage (rate-limit suite: 11 tests pass).

…ient IPs

Replace the per-process Map with a shared atomic store so limits survive
restarts and hold across replicas. A Redis-backed store (INCR + PEXPIRE in
a Lua script) is used when REDIS_URL is set, with an in-memory fallback for
development and tests; all four limiters share one store instance.

Client identity now comes from the socket address by default, and only the
rightmost x-forwarded-for hop is trusted when TRUST_PROXY is enabled, so
clients can no longer rotate headers to reset their budget. Docs and
.env.example document the new REDIS_URL and TRUST_PROXY settings.
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.

Rate limiter is a per-process Map keyed by a spoofable header: limits reset on restart and are per-replica

1 participant