Skip to content

3rd-audit closeout + adversarial-review hardening (F13 double-spend, worker guard, F21)#459

Merged
MikeyPetrillo merged 5 commits into
mainfrom
claude/sweet-brown-i99jl3
Jul 19, 2026
Merged

3rd-audit closeout + adversarial-review hardening (F13 double-spend, worker guard, F21)#459
MikeyPetrillo merged 5 commits into
mainfrom
claude/sweet-brown-i99jl3

Conversation

@MikeyPetrillo

Copy link
Copy Markdown
Owner

Closes the last code-actionable items of the 3rd security audit and hardens what was built, after an independent adversarial self-review of the whole session's work.

Findings closed

  • F21 (Low) — bounded the one-time-IP rate-limit maps (waitlistHits, regByIp) with a periodic + inline stale sweep, and added a global waitlist ceiling. src/rate-sweep.js + 10 unit tests.
  • F13 (Med) — idempotent durable credit for capacity-refused paid renders, flagged OFF (RENDER_CREDIT_ENABLED, default off → billing byte-identical). On a 503 of a settled paid render/screenshot the server mints a one-time 256-bit credit token, returned only in that buyer's 503, bound to route+body; a retry presenting it is served without a second charge. src/render-credit.js + 24 unit tests.
  • Audit closeout ledger docs/audit-closeout-f01-f25.md: 23/25 Fixed, F22 Accepted (Low CSP inline, no live XSS), and copy-paste owner steps for F01 rulesets, F17 DMARC, F25 DNS.

Adversarial self-review (this is the important part)

Four independent auditor agents were told to hunt for exploits. The earlier batches (F03/F09/F10/F12/F14/F15/F16/F20/F23/F24) and F21/F11/F07 verified clean; the F04 DNS-rebinding defense reviewed solid. Three real issues in code added this session were found and fixed here:

  1. F13 double-spend (TOCTOU) — validate-then-consume-on-finish let concurrent retries of one token each be served. Fixed with atomic claim() (validate-and-remove in one synchronous step) + re-issue on failed delivery. Test asserts exactly 1 of 100 concurrent claims wins. F13 was flag-OFF, so prod was never exposed.
  2. Worker boot guard (C1a) — was an incomplete denylist while the app reads 30+ secret vars. Now pattern-based fail-closed (GITHUB_TOKEN/E2B_API_KEY/burners/provider keys all block boot), enforced at boot only (import-safe). Railway health-gates the deploy, so a false positive fails the new deploy, not prod.
  3. Worker /call auth (C1b) — failed open on an empty token. Now fails closed + the worker refuses to boot without RENDER_WORKER_TOKEN. Plus Object.hasOwn dispatch and --proxy-bypass-list=<-loopback> so metadata/loopback literals also traverse the F04 egress proxy.

Tests

New offline suites wired into CI: test-rate-sweep.js (10), test-render-credit.js (24, incl. concurrent-claim double-spend), test-egress-proxy.js (12), test-worker-isolation.js (17). Full test-all clean (214 strict + 284 lenient, 0 failures).

Rollback

F13 stays OFF unless RENDER_CREDIT_ENABLED is set. Worker hardening is health-gated. No settlement path changed while the flag is off.

MikeyPetrillo and others added 5 commits July 19, 2026 08:57
… ceiling

The per-IP hit maps (waitlistHits, regByIp) only pruned an entry when the SAME
IP returned, so a flood of one-time IPs accumulated keys without bound; the
waitlist also had only a per-IP limit, so a distributed source (many IPs each
under the limit) could mass-insert leads.

src/rate-sweep.js (unit-tested, 10 cases):
- sweepStaleTsMap(map, windowMs) evicts keys whose timestamps are all stale.
- makeWindowCounter(windowMs, limit) is a global rolling-window ceiling.

server.js wiring:
- waitlist: a global ceiling (60/min across ALL IPs, checked after the per-IP
  gate) + an inline stale-sweep backstop when the map exceeds 5000 keys.
- register: inline stale-sweep backstop (it already had a 30/hr global cap).
- a 60s periodic sweep of both maps (mirrors the powChallengeHits /
  operatorSessions sweeps).

Also adds docs/audit-closeout-f01-f25.md — the final ledger for the 3rd audit:
21/25 Fixed, F22 Accepted (Low, CSP inline; no live XSS, nonce refactor too risky
for the severity), and copy-paste owner steps for the 4 owner-gated items
(F01 rulesets, F13 capacity decision, F17 DMARC, F25 DNSSEC/CAA/MTA-STS/www).

full test-all clean (214 strict + 284 lenient, 0 failures).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d rate maps) + audit closeout

F21 (Low) — bound the one-time-IP rate maps (waitlistHits, regByIp) with a
periodic + inline stale sweep and add a global waitlist ceiling
(src/rate-sweep.js, 10 unit tests). Closes the memory-growth + distributed
mass-submit finding.

F13 (Med) — idempotent durable credit for capacity-refused paid renders,
approach B, FLAGGED OFF (RENDER_CREDIT_ENABLED, default off → billing
byte-identical). x402 settles before the handler, so a paid render/screenshot
that then 503s on the bounded Chromium pool was charged but not served. On such a
503 the server mints a one-time 256-bit credit TOKEN, returned only in that
buyer's own 503 (X-Render-Credit header + renderCredit body), bound to
route+body; a retry presenting the token skips the gate and is served without a
second charge, consumed only on 200, surviving repeated 503s.

Security: the token is a server-minted bearer secret handed only to the paying
caller (NOT the public payer address), so a third party knowing a wallet can't
forge or steal it; bound to route+body so it can't be spent elsewhere. Verified
live that a forged token still 402s. src/render-credit.js + 21 unit tests
(single-use, forgery/cross-request rejection, TTL, 503-persistence, eviction).
Boot verified flag on and off; full test-all clean (214 strict + 284 lenient).

docs/audit-closeout-f01-f25.md — final ledger for the 3rd audit: 23/25 Fixed
(F13 flag-off), F22 Accepted (Low CSP inline; no live XSS), and copy-paste owner
steps for F01 rulesets, F17 DMARC, F25 DNSSEC/CAA/MTA-STS/www.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ouble-spend + worker guard)

Independent auditor agents reviewed everything built this session. The earlier
audit batches (F03/F09/F10/F12/F14/F15/F16/F20/F23/F24) and F21/F11/F07 verified
clean. Three real issues were found in code added this session and are fixed here
(F13 was flag-OFF, so prod was never exposed):

F13 render-credit — CONFIRMED double-spend (TOCTOU). The credit was validated as
a read (valid()) and consumed on res 'finish', so a burst of concurrent retries
with the same token all passed validation before any finished → many free
renders from one paid credit. FIX: atomic admission — claim() validates AND
removes the token in one synchronous step; only the first of N concurrent retries
wins, the rest fall through to the paywall. A failed pre-paid retry re-issues a
fresh token so the credit isn't lost. Test asserts exactly 1 of 100 concurrent
claims succeeds (24 cases).

Worker isolation (F02/F04/F06):
- C1a: the boot guard was an incomplete denylist (12 names) while the app reads
  30+ secret env vars. Now a pattern-based fail-closed guard
  (KEY|SECRET|TOKEN|MNEMONIC|PRIVATE|PASSWORD|CREDENTIAL + non-pattern extras) so
  an unlisted secret (GITHUB_TOKEN, E2B_API_KEY, provider keys, burners) also
  blocks boot. Enforced at boot only (isMain), never on import, so a CI process
  with its own GITHUB_TOKEN can still import the exports. Railway health-gates
  the deploy, so a false positive fails the new deploy, not prod.
- C1b: /call auth failed OPEN when RENDER_WORKER_TOKEN was empty. Now fails
  closed, and the worker refuses to boot without a token.
- Tool dispatch uses Object.hasOwn so slug "constructor"/"toString" can't hit an
  inherited prototype function.
- F04 defense-in-depth: Chromium launched with --proxy-bypass-list=<-loopback>
  so loopback/link-local literals (incl 169.254.169.254 metadata) also traverse
  the validating egress proxy, making it the single chokepoint.

The DNS-rebinding defense itself reviewed SOLID (no hole). Full test-all clean
(214 strict + 284 lenient, 0 failures); all offline security suites green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…RAILWAY_PRIVATE_DOMAIN)

The pattern-based boot guard matched RAILWAY_PRIVATE_DOMAIN — Railway's own
private-network hostname, not a secret — via the bare "PRIVATE" token, so the
hardened worker refused to boot. Railway's health-gated rollout caught it (new
deploy failed, old worker kept serving; prod render unaffected).

Fix: drop "PRIVATE" from the pattern (a private KEY is already caught by "KEY";
matching "PRIVATE" alone false-positives on the infra var). Extract the guard
logic into an exported pure `forbiddenSecretsIn(env)` and unit-test the exact
regression: the secretless worker env (incl. RAILWAY_PRIVATE_DOMAIN + its own
token) does NOT trip the guard, while GITHUB_TOKEN / E2B_API_KEY / burners /
provider keys / WALLET_ADDRESS / DATABASE_URL still do. 27/27 offline; a clean
Railway-like env boots green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@MikeyPetrillo
MikeyPetrillo temporarily deployed to agent402 / production July 19, 2026 13:58 — with GitHub Actions Inactive
@railway-app
railway-app Bot temporarily deployed to agent402 / production July 19, 2026 13:58 Inactive
@MikeyPetrillo
MikeyPetrillo merged commit 4508450 into main Jul 19, 2026
16 checks passed
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