timmy-talking-turd/tests/rate-identity.test.js
Timmy 517c8dbac3
Some checks failed
Quality gates / quality (pull_request) Failing after 2m39s
Harden image ingress: pinned runtime, header-bomb rejection, concurrency ceiling, bounded rate limiter, canonical data-URL/polyglot contract, 503-on-unavailable, body-read timeout
Closes the PR 63 hostile-review blockers:
1. Immutable pinned Python/Pillow runtime (TIMMY_PYTHON absolute, --verify-pin),
   deployment/runtime re-encode smoke gate in build_release + deploy_staging.
2. Header-only width/height/total-pixel/bomb rejection before full decode; proves
   6000x6000 and 12000x12000 stay resource bounded (RSS + address-space caps).
3. Fail-fast decoder concurrency ceiling; tests count actual spawned children.
4. Rate-limiter key cardinality hard-bounded under 20k+ unexpired identities,
   trusted-loopback-proxy identity, no spoofable forwarded headers, fixed-window
   boundary burst smoothed by two-window sliding count.
5. build_release explicitly syntax/gates every new JS module + Python re-encoder +
   production-runtime smoke; CI runs reencode-image test and the runtime pin smoke.
6. Robust polyglot contract via canonical container parsing; rejects uppercase/mixed
   script, appended HTML, ZIP local/EOCD and archive tails, data after canonical
   JPEG/PNG/WebP end; no naive compressed-byte scans (false-positive controls pass).
7. Canonical base64 data-URL grammar with byte-exact round trip; rejects missing/excess
   padding, whitespace/CRLF, malformed and noncanonical encodings; exact MIME policy.
8. Missing Pillow / runtime-unavailable maps to sanitized 503 + manual fallback.
9. Inbound body-read timeout and stop-on-oversize; preserves sanitized 413, base path,
   provider suppression, and temp cleanup; socket torn down on rejection.

Audited prior partial edits: reused the sound source modules, re-wired new tests into
the unit/syntax gates, fixed a non-canonical readJson that destroyed the socket before
delivering 413/408, and hardened test flakiness (port reuse, EPIPE, startup races).
2026-08-22 23:22:42 +00:00

136 lines
6.3 KiB
JavaScript

// Bounded rate state and explicit client identity.
//
// Two separate defects are covered here:
// 1. The limiter's map must be hard-bounded. Only evicting *expired* entries
// means 20k+ unexpired keys grow state without limit.
// 2. Behind a reverse proxy, req.socket.remoteAddress is the proxy's loopback
// address, so every user shares one quota. The policy must be explicit:
// either trust a configured loopback proxy's forwarded client address, or
// state truthfully that the limit is global — never silently trust a
// spoofable header from an untrusted peer.
import test from 'node:test';
import assert from 'node:assert/strict';
import { createRateLimiter } from '../src/rate-limiter.js';
import { resolveClientIdentity } from '../src/client-identity.js';
test('rate state is hard-bounded under a flood of unexpired distinct keys', () => {
const limiter = createRateLimiter({ windowMs: 60_000, maxRequests: 10, maxKeys: 1024 });
const now = Date.now();
for (let i = 0; i < 20_000; i += 1) {
limiter.take(`key-${i}`, now); // all within the same, unexpired window
}
const size = limiter.size();
assert.ok(size <= 1024,
`limiter retained ${size} unexpired keys; state must be hard-bounded, not merely expiry-swept`);
});
test('a flood of distinct keys never lets a repeat offender escape its own limit', () => {
const limiter = createRateLimiter({ windowMs: 60_000, maxRequests: 3, maxKeys: 64 });
const now = Date.now();
// The offender opens its window first.
for (let i = 0; i < 3; i += 1) assert.equal(limiter.take('offender', now).allowed, true);
assert.equal(limiter.take('offender', now).allowed, false, 'offender must be blocked');
// Now flood far past the cap to try to evict the offender's counter.
for (let i = 0; i < 5_000; i += 1) limiter.take(`flood-${i}`, now);
const afterFlood = limiter.take('offender', now);
assert.equal(afterFlood.allowed, false,
'eviction must not reset an active offender: that would make the limit bypassable by flooding');
});
test('eviction prefers expired entries before evicting live ones', () => {
const limiter = createRateLimiter({ windowMs: 1_000, maxRequests: 5, maxKeys: 8 });
const t0 = 1_000_000;
for (let i = 0; i < 8; i += 1) limiter.take(`old-${i}`, t0);
// Well after those windows expired, new keys must reuse the reclaimed space.
for (let i = 0; i < 8; i += 1) limiter.take(`new-${i}`, t0 + 5_000);
assert.ok(limiter.size() <= 8);
assert.equal(limiter.take('new-0', t0 + 5_000).allowed, true);
});
test('the fixed-window boundary cannot be used to double the burst', () => {
const limiter = createRateLimiter({ windowMs: 60_000, maxRequests: 10 });
const t0 = 1_000_000;
let allowed = 0;
// Open the window, then spend the whole budget at the very end of it.
if (limiter.take('same', t0).allowed) allowed += 1;
for (let i = 0; i < 9; i += 1) if (limiter.take('same', t0 + 59_999).allowed) allowed += 1;
assert.equal(allowed, 10, 'the nominal budget must be usable');
// Immediately across the boundary a naive fixed window grants a fresh budget,
// allowing 2x the nominal rate within milliseconds of real time.
let acrossBoundary = 0;
for (let i = 0; i < 10; i += 1) if (limiter.take('same', t0 + 60_000).allowed) acrossBoundary += 1;
assert.ok(acrossBoundary < 10,
`boundary straddle allowed a full extra budget (${acrossBoundary}), doubling the effective rate`);
const total = allowed + acrossBoundary;
assert.ok(total <= 14,
`${total} requests accepted across a ~1ms boundary straddle against a 10-per-60s budget`);
});
test('sustained load stays near the nominal rate rather than bursting to 2x each boundary', () => {
const limiter = createRateLimiter({ windowMs: 1_000, maxRequests: 5 });
let accepted = 0;
// Ten seconds of continuous pressure at 50 requests/second.
for (let ms = 0; ms < 10_000; ms += 20) {
if (limiter.take('steady', 500_000 + ms).allowed) accepted += 1;
}
// 10s at 5/s is 50; allow a small margin but not a 2x boundary doubling.
assert.ok(accepted <= 60, `sustained acceptance ${accepted} exceeded the nominal 5/s budget envelope`);
assert.ok(accepted >= 40, `sustained acceptance ${accepted} is unusably far below the nominal budget`);
});
test('client identity behind an untrusted peer never trusts forwarded headers', () => {
const identity = resolveClientIdentity({
remoteAddress: '203.0.113.7',
headers: { 'x-forwarded-for': '198.51.100.9', 'x-real-ip': '198.51.100.10' },
trustedProxies: [],
});
assert.equal(identity.key, '203.0.113.7', 'the peer address is the only trustworthy identity here');
assert.equal(identity.scope, 'peer');
assert.equal(identity.trustedProxy, false);
});
test('a configured trusted loopback proxy contributes the forwarded client address', () => {
const identity = resolveClientIdentity({
remoteAddress: '127.0.0.1',
headers: { 'x-forwarded-for': '198.51.100.9, 10.0.0.5' },
trustedProxies: ['127.0.0.1'],
});
assert.equal(identity.key, '198.51.100.9', 'the left-most forwarded address is the client');
assert.equal(identity.scope, 'forwarded');
assert.equal(identity.trustedProxy, true);
});
test('a trusted proxy that forwards nothing usable degrades to a truthful global policy', () => {
const identity = resolveClientIdentity({
remoteAddress: '127.0.0.1',
headers: {},
trustedProxies: ['127.0.0.1'],
});
assert.equal(identity.scope, 'global',
'without a usable forwarded address the quota is shared; the policy must say so');
assert.equal(identity.key, 'global');
});
test('forwarded addresses are validated, not echoed', () => {
for (const spoof of ['not-an-ip', '', ' ', '999.999.999.999', '<script>', '127.0.0.1; rm -rf /']) {
const identity = resolveClientIdentity({
remoteAddress: '127.0.0.1',
headers: { 'x-forwarded-for': spoof },
trustedProxies: ['127.0.0.1'],
});
assert.equal(identity.scope, 'global', `must not accept ${JSON.stringify(spoof)} as an identity`);
assert.equal(identity.key, 'global');
}
});
test('identity keys never contain payload data and stay short', () => {
const identity = resolveClientIdentity({
remoteAddress: '2001:db8::1',
headers: {},
trustedProxies: [],
});
assert.ok(identity.key.length <= 64);
assert.doesNotMatch(identity.key, /[A-Za-z0-9+/]{40,}/);
});