Skip to content

fix: contain handler errors after headers sent + harden nested-router state - #56

Merged
jkyberneees merged 1 commit into
masterfrom
fix/nested-state-and-error-hardening
Sep 6, 2026
Merged

fix: contain handler errors after headers sent + harden nested-router state#56
jkyberneees merged 1 commit into
masterfrom
fix/nested-state-and-error-hardening

Conversation

@jkyberneees

Copy link
Copy Markdown
Collaborator

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 as uncaughtException (sync) or unhandledRejection (async) and killing the whole server.

  • lib/next.js: all 6 error-handler dispatch sites now route through dispatchError()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_HANDLER guards res.headersSent.

🟠 HIGH — grandchild nesting corrupted parent request state

Single-slot preRouterUrl was overwritten per nesting level; the cleanup middleware re-read it after deeper levels deleted it → parent saw url=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

  • New 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)

  • Case-insensitive route matching (behavior change → major).
  • Nested-router no-match terminates the response (semantic change → feature release).
  • get()-mounted routers not rewritten (docs note added).

Review

3 adversarial sub-agent reviewers (security/perf/correctness + 2 fix-review passes + final clean gate).

… 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.
@jkyberneees
jkyberneees merged commit 3fb4f50 into master Sep 6, 2026
5 checks passed
@jkyberneees
jkyberneees deleted the fix/nested-state-and-error-hardening branch September 6, 2026 13:41
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