Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions app/middleware/rate-limit-key.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@
*
* IPv6 handling: express-rate-limit v8+ refuses to start unless any
* custom keyGenerator that touches `req.ip` routes the value through
* `ipKeyGenerator(req, res)`. The helper canonicalizes IPv4 and IPv6
* addresses (matching them to a /64 prefix on IPv6 to prevent a
* single client from bypassing limits by rotating through their /64
* allocation). Without this wrapper, IPv6 clients could each present
* a unique address and slip past the per-IP budget.
* `ipKeyGenerator(ip)`. The helper canonicalizes IPv4 addresses
* (returning them verbatim) and IPv6 addresses (collapsing them to
* their /56 network prefix — the helper's default `ipv6Subnet`).
* Without this wrapper, an IPv6 client could present a fresh address
* from anywhere inside their allocation on every request and slip
* past the per-IP budget.
*
* Exported separately from server.js so unit tests can exercise
* the keying directly without spinning up an HTTP server.
Expand All @@ -39,8 +40,9 @@ function keyByAuthKeyOrIp(req /*, res */) {
}
// express-rate-limit v8+ requires the helper. It takes the raw
// IP string and returns the IPv4 address verbatim or the IPv6
// /64 prefix. Fall back to 'unknown' when no source IP is
// available (e.g. unit-test fixtures or non-IP transports).
// /56 network prefix (the helper's default). Fall back to
// 'unknown' when no source IP is available (e.g. unit-test
// fixtures or non-IP transports).
const ip = req.ip || (req.connection && req.connection.remoteAddress);
if (!ip) return 'ip:unknown';
return 'ip:' + ipKeyGenerator(ip);
Expand Down
19 changes: 19 additions & 0 deletions tests/unit/rate-limit-key.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,23 @@ describe('keyByAuthKeyOrIp', () => {
const b = keyByAuthKeyOrIp(fakeReq({ ip: '1.1.1.1' }));
expect(a).toBe(b);
});

test('IPv6 addresses in the same /56 collapse to the same key', () => {
// An attacker rotating through addresses inside their ISP
// allocation would defeat per-IP rate limiting if the key
// were the raw IP. express-rate-limit's ipKeyGenerator
// canonicalizes IPv6 to its /56 network (helper default),
// so two different addresses inside one /56 must bucket to
// the same key.
const a = keyByAuthKeyOrIp(fakeReq({ ip: '2001:db8:1234:5600::1' }));
const b = keyByAuthKeyOrIp(fakeReq({ ip: '2001:db8:1234:56ff::dead' }));
expect(a).toBe(b);
expect(a.startsWith('ip:')).toBe(true);
});

test('IPv6 addresses in different /56 prefixes get different keys', () => {
const a = keyByAuthKeyOrIp(fakeReq({ ip: '2001:db8:1234:5600::1' }));
const b = keyByAuthKeyOrIp(fakeReq({ ip: '2001:db8:1234:5700::1' }));
expect(a).not.toBe(b);
});
});