fix: contain handler errors after headers sent + harden nested-router state - #56
Merged
Merged
Conversation
… state Adversarial review (20260906) findings, each verified by RED-first tests: - CRITICAL: handler errors thrown after headers were flushed crashed the process (uncaughtException/unhandledRejection via ERR_HTTP_HEADERS_SENT). All error-handler dispatch sites now route through dispatchError(); the default handler guards res.headersSent. - HIGH: grandchild nesting restored undefined url/path into the parent chain (single-slot preRouterUrl). Cleanup/error now restore from closure snapshots taken before each lookup level. - HIGH: nested router params leaked into the parent scope via in-place Object.assign. Params are now per-level merge copies, restored on exit. - MEDIUM: parametrized matches were cached under unbounded distinct keys, pinning attacker-growable memory and evicting hot static entries. Matches with params are no longer cached.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes 4 findings from an adversarial security & performance review (3 sub-agent hunters + personal PoC verification; full report in
sec-findings.md). All changes verified RED-first and reviewed by 3 adversarial reviewers (2 substantive passes surfaced 6 follow-up fixes, final pass clean).What changed
🔴 CRITICAL — process crash on error after headers sent
A route handler that throws/rejects after flushing headers made the default error handler throw
ERR_HTTP_HEADERS_SENT, escaping asuncaughtException(sync) orunhandledRejection(async) and killing the whole server.lib/next.js: all 6 error-handler dispatch sites now route throughdispatchError()→failSafe()(headersSent guard, bare 500 fallback, socket teardown). Async custom error handlers that reject are contained too (Promise.resolve(result).catch(failSafe)).lib/router/sequential.js:DEFAULT_ERROR_HANDLERguardsres.headersSent.🟠 HIGH — grandchild nesting corrupted parent request state
Single-slot
preRouterUrlwas overwritten per nesting level; the cleanup middleware re-read it after deeper levels deleted it → parent sawurl=undefined. Cleanup/error now restore from closure snapshots taken before each lookup level.🟠 HIGH — params leaked between parent and nested routers
Object.assign(req.params, …)mutated the parent's params object in place. Params are now per-level merge copies (child still inherits parent params — existing behavior preserved), restored on exit.🟡 MEDIUM — unbounded cache memory (DoS) + param-route churn
Parametrized matches, 404s, and matches with params were cached under unbounded distinct keys — 50k junk paths pinned ~373MB. Fix: never cache param/404 matches, plus byte-bound the LRU (
maxSize+maxEntrySize: 4096) so long paths matched by global middleware or catch-all regexes can't pin memory either.Tests
tests/security-hardening.test.js— 10 tests (crash containment sync/async/custom, deep-nesting restore, params isolation, cache-skip + memory-bound).npm test: 93 passing, 1 pending, 98.36% lines (gate 85%). Lint (standard) clean.Scope-out (documented, deferred)
get()-mounted routers not rewritten (docs note added).Review
3 adversarial sub-agent reviewers (security/perf/correctness + 2 fix-review passes + final clean gate).