Skip to content

feat(protect): thread request into the response phase + cheap prefilter for response rules - #106

Merged
patchstackdave merged 2 commits into
mainfrom
feat/response-phase-request-context
Aug 12, 2026
Merged

feat(protect): thread request into the response phase + cheap prefilter for response rules#106
patchstackdave merged 2 commits into
mainfrom
feat/response-phase-request-context

Conversation

@patchstackdave

Copy link
Copy Markdown
Contributor

Why

The response phase built its engine input from _response alone, with no originating request. So a response rule's when: { path, method } scope resolved server.REQUEST_URI/REQUEST_METHOD to defaults (/, GET) and was inert — a route-scoped response rule silently never matched its route (flagged in the output-filtering review, issue #6).

This is the single highest-leverage engine change for output filtering: it repairs when scoping and is the prerequisite for the response rules people actually want next.

What

Thread a minimal request context — method / originalUrl / request headers — from every screening entry point into screenText, spread alongside _response:

  • .fetch(handler)reqContextFromFetch(request)
  • .express() / .node() with screenResponsesreqContextFromNode(req)
  • public screenResponse(response, request) (new optional 2nd arg; Supabase guard can pass it)

No body is threaded — response rules key on request method/path/Host/Origin, not the request body.

Unlocks (follow-ups, not in this PR)

  • Open-redirect: compare a 3xx Location host to the request Host.
  • CORS reflection: flag ACAO: */reflected-Origin + ACAC: true (needs AND-across-two-headers in a response rule).
  • IDOR field-scoping: mask leaves not owned by the caller (needs request identity + a relational match type).

Compatibility & tests

Backward-compatible — unscoped response rules and callers that pass no request are unchanged. tests/protect/response-when-scope.test.ts (4 new): route match / non-match / method non-match / unscoped-no-request. 599 pass; typecheck clean.

🤖 Generated with Claude Code

The response phase evaluated rules against a phantom request built from `_response` alone, so a
response rule's `when: { path, method }` scope resolved REQUEST_URI/METHOD to defaults ('/','GET')
and was effectively inert — a route-scoped response rule never matched its route.

Thread a minimal request context (method / originalUrl / request headers) from every screening
entry point — .fetch(handler), .express()/.node() (screenResponses), and the public
screenResponse(response, request) — into screenText, spread alongside `_response`. Now:

- response rules honour `when` route/method scope (the enabling fix), and
- request Host/Origin are visible to response rules — the foundation for open-redirect
  (Location vs Host), CORS reflection, and IDOR field-scoping rules (follow-ups).

Backward-compatible: unscoped response rules and callers that pass no request are unchanged.
599 tests pass (+4 new); typecheck clean.

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

coderbuds Bot commented Aug 12, 2026

Copy link
Copy Markdown

Adds response prefilter and request context threading for route-scoped rules.

🎯 Quality: 100% Elite · 📦 Size: Small

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

See how your team is trending →

@patchstackdave

Copy link
Copy Markdown
Contributor Author

/review

Response screening runs one regex pass per rule over the (up to 512 KiB) body — ~9 default
rules means ~9 full passes on every response, even when there is obviously no secret. A rule
may now declare `prefilter: [literal, ...]`; screenText runs the rule's regex only when at
least one anchor is present in the body (case-insensitive, body lowercased once, lazily). Bodies
with no candidate — the common case — skip the scan entirely, cutting CPU/latency and shrinking
the regex/ReDoS surface. Rules without a prefilter are unchanged.

+4 tests; 603 pass; typecheck clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@patchstackdave patchstackdave changed the title feat(protect): thread the originating request into the response phase (route-scoped response rules) feat(protect): thread request into the response phase + cheap prefilter for response rules Aug 12, 2026
@patchstackdave
patchstackdave merged commit dc75987 into main Aug 12, 2026
4 checks passed
@patchstackdave
patchstackdave deleted the feat/response-phase-request-context branch August 12, 2026 11:03
patchstackdave added a commit that referenced this pull request Aug 12, 2026
Add an `off_origin` response-phase primitive: true (→ block) when a 3xx redirect's Location
header points to a different origin than the request's own Host. Relative / same-origin
Locations never match; lenient when the request Host is unknown. This is the first capability
unlocked by threading the request into the response phase (#106).

Also derive the request Host from the URL in reqContextFromFetch — a fetch Request doesn't
expose a Host header, and origin-comparing response rules (open-redirect, later CORS) need it.

Not a default rule (many apps redirect off-site legitimately); it's authored and route-scoped
via `when`. +6 tests; 632 pass; typecheck clean.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
patchstackdave added a commit that referenced this pull request Aug 12, 2026
…tection

Add a `cors_reflected` response-phase primitive (dispatched like cross_origin/off_origin): true
(→ block) when a response allows credentials AND either uses `Access-Control-Allow-Origin: *` or
reflects the caller's own Origin — the combination that lets any malicious site read the
authenticated response. A fixed allowlisted origin, or reflection without credentials, is not
flagged. Uses the request Origin threaded into the response phase (#106).

Third origin-comparison primitive after cross_origin (CSRF) and off_origin (open-redirect). Not a
default; authored + route-scoped via `when`. +6 tests; 632 pass; typecheck clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
patchstackdave added a commit that referenced this pull request Aug 12, 2026
…tection (#110)

Add a `cors_reflected` response-phase primitive (dispatched like cross_origin/off_origin): true
(→ block) when a response allows credentials AND either uses `Access-Control-Allow-Origin: *` or
reflects the caller's own Origin — the combination that lets any malicious site read the
authenticated response. A fixed allowlisted origin, or reflection without credentials, is not
flagged. Uses the request Origin threaded into the response phase (#106).

Third origin-comparison primitive after cross_origin (CSRF) and off_origin (open-redirect). Not a
default; authored + route-scoped via `when`. +6 tests; 632 pass; typecheck clean.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.

3 participants