Skip to content

protect: input-handling robustness pass (canonicalization + edge cases) - #114

Merged
patchstackdave merged 3 commits into
mainfrom
protect/input-handling-hardening
Aug 12, 2026
Merged

protect: input-handling robustness pass (canonicalization + edge cases)#114
patchstackdave merged 3 commits into
mainfrom
protect/input-handling-hardening

Conversation

@patchstackdave

Copy link
Copy Markdown
Contributor

What

A defensive hardening pass so rule matching stays correct on inputs that don't take the obvious shape — alternate encodings, unusual structure, and content-type/edge-case handling — across the engine and the request/response runtime. No public API changes; existing behavior is preserved on ordinary inputs.

Highlights:

  • Host/IP classification (internal_host) canonicalizes before deciding — every IPv4 spelling inet_aton accepts, plus expanded / IPv4-mapped IPv6 forms, are recognized (was a string/prefix compare). Adds 100.64.0.0/10. False-positive guards for ordinary hostnames retained.
  • Origin / redirect comparisons resolve against the request origin and normalize default ports: off_origin handles protocol-relative and backslash Locations; cross_origin distinguishes a truly-absent header from a present opaque one and fixes a :443-vs-elided-port false positive; cors_reflected covers ACAO: null + credentials.
  • Structured values: scalar matchers now fan out over the leaves of a nested / array-of-object value instead of stringifying it to [object Object]; the walk is iterative and bounded, so a pathological value can't throw a RangeError that fails a rule open.
  • Regex safety: the ReDoS detector catches nested quantified subgroups (e.g. ((ab)+)+), and a rejected pattern now warns that its rule is unenforced instead of failing silently.
  • Request body: permissive content-type parsing (*+json, text/plain, and no-content-type JSON bodies now populate post.<field>); body inspection is no longer skipped based on a declared Content-Length; all folds in the verbatim body; and request normalization no longer deletes line-comment spans from the value it inspects.
  • Response screening: exact content-type matching for live streams (a param containing event-stream no longer disables screening), a binary sniff so a textual octet-stream export is screened while real binaries pass through untouched, and a redactor whose rule decodes the body before matching now fails closed rather than serving a no-op mask.
  • Whitelist misconfiguration (rule_id-less entry, unimplemented whitelist_keys) now warns.

Tests

Adds tests/protect/security-hardening.test.ts (canonicalization tables, structured-value matching, origin checks, content-type handling, regex guard, redactor fail-closed) and updates the normalizer / response-guards suites for the corrected behavior. 703 tests green, typecheck clean, build clean, examples/protect demo passes.

Notes

A few residual behaviors are intentional trade-offs (documented in-code): the response size cap still fails open above the screening ceiling (rule authors opt into bypass_limit / max_bytes), a pre-gzipped body isn't decompressed for screening, and the encode action is HTML-text-context only (use block for URI-scheme contexts).

A batch of defensive hardening so matching stays correct on inputs that don't
take the obvious shape. No public API changes.

- Host/IP classification (internal_host) now canonicalizes before deciding:
  every IPv4 spelling inet_aton accepts, and expanded / IPv4-mapped IPv6 forms,
  are recognized (previously a string/prefix compare); adds 100.64.0.0/10.
- Origin/redirect comparisons resolve against the request origin and normalize
  default ports (off_origin handles protocol-relative / backslash locations;
  cross_origin distinguishes an absent header from a present opaque one;
  cors_reflected covers ACAO: null + credentials).
- Scalar matchers fan out over the leaves of a structured (nested / array-of-
  object) value instead of stringifying it; bounded + iterative so a
  pathological value can't fail a rule open.
- Regex safety: the ReDoS detector catches nested quantified subgroups, and a
  rejected pattern now warns (the rule is unenforced) instead of failing silent.
- Request body handling: permissive content-type parsing (+json / text/plain /
  no content-type still populate post.<field>); body inspection is no longer
  skipped on a declared Content-Length; `all` folds in the verbatim body.
- Request normalization no longer deletes line-comment spans from the value it
  inspects (that hid payloads from parameter-scoped rules).
- Response screening: exact content-type matching for live streams, a binary
  sniff so a textual octet-stream export is screened; a redactor whose rule
  decodes the body before matching now fails closed rather than serving a no-op
  mask; whitelist misconfig (no rule_id / unimplemented keys) warns.

Adds tests/protect/security-hardening.test.ts plus updates to the normalizer /
response-guards suites. 703 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderbuds

coderbuds Bot commented Aug 12, 2026

Copy link
Copy Markdown

Robust input-handling improvements with extensive canonicalization and detailed edge-case tests.

🎯 Quality: 96% Elite · 📦 Size: Extra Large — strongly consider breaking this down

🛡️ Standards: no pre-flight fit check ran for this change — wire assess-change-fit into your coding agents to catch size before opening.

📈 This month: Your 50th PR — above team average · Averaging Excellent

See how your team is trending →

patchstackdave and others added 2 commits August 12, 2026 16:23
…pth test

The request normalizer recursed into nested objects unbounded, so a
pathologically deep value could overflow the stack before matching ran — the
per-rule catch would swallow that into a fail-open. Cap the walk (values below
the bound are left un-normalized, still matched, never crashing).

The regression test built its deep value via a JSON string, which overflowed
JSON.parse/stringify on Node 18/20/22 (but not 25) — a test artifact, not the
engine. Rebuild it in memory and assert it still matches past the normalize cap
(a fail-open crash would return blocked:false). Verified on Node 20 and 22.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CI matrix tested 18/20/22, but publish.yml and release.yml build on Node
24 — so releases ran on a version CI never exercised. Add 24.x to close that gap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@patchstackdave

Copy link
Copy Markdown
Contributor Author

/review

@patchstackdave
patchstackdave merged commit e69337e into main Aug 12, 2026
5 checks passed
@patchstackdave
patchstackdave deleted the protect/input-handling-hardening branch August 12, 2026 14:29
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.

2 participants