Skip to content

feat(protect): rule-driven Tier-3 (response redaction + egress SSRF) + array-param fix#57

Merged
patchstackdave merged 2 commits into
mainfrom
feat/protect-tier3-response-egress
Jul 14, 2026
Merged

feat(protect): rule-driven Tier-3 (response redaction + egress SSRF) + array-param fix#57
patchstackdave merged 2 commits into
mainfrom
feat/protect-tier3-response-egress

Conversation

@patchstackdave

Copy link
Copy Markdown
Contributor

Brings the output-side protection into the vendored @patchstack/connect/protect runtime, fully rule-driven (nothing hardcoded) via rule phases.

New capabilities

  • Rule properties: phase (request (default) / response / egress) + category.
  • Engine (src/protect/engine): resolves response.status|body|header.* and egress.url|host|method; new internal_host match type (private / loopback / link-local / cloud-metadata ranges).
  • Runtime (createProtection): the fetch path now screens the response; opt-in egress: true wraps global fetch to screen the app's outbound calls (SSRF). Both driven by default bundles in defaults.js, overridable via responseRules / egressRules.

Response leak handling — per-rule action

  • redact (default for the secret rules) — mask only the offending span(s) and still serve the page. A legitimate response that accidentally leaks one key gets that key masked ([REDACTED]), not the whole response withheld. Mask via maskWith.
  • block — withhold the whole response.

Enforcement only in block mode; dry-run observes (onDetect gets phase/category) and serves the original. Fail-open throughout. createSupabaseGuard / createServerFnGuard unchanged.

Bug fix (found by the test-bed)

Array parameter (["get.a","post.b"]) — used pervasively in rule_v2silently never matched: resolve() did parameter.indexOf('.'), which on an array returns -1. #evaluateCondition now resolves each element and ORs them. Regression covered.

Tests / build

tests/protect.test.ts +6 (array params, response redact/block, egress block/dry-run). Full suite 151 pass, typecheck clean, build bundles defaults.js/egress.js into dist/protect.js. No changes to the install prompt / AGENT-INSTALL / guide surface (no field-test impact).

🤖 Generated with Claude Code

…SSRF; array params

Brings the output-side protection into the vendored runtime, as RULES (nothing hardcoded),
via rule phases:
- new rule properties: `phase` (request|response|egress, default request) + `category`
- engine: resolve `response.status|body|header.*` and `egress.url|host|method`; new
  `internal_host` match type (private/loopback/link-local/metadata ranges)
- runtime (createProtection): response phase screens the outgoing body; egress phase
  (opt-in `egress:true`) wraps global fetch to screen outbound calls. Driven by default
  bundles in defaults.js (overridable via responseRules/egressRules).

Response leak handling supports per-rule `action`:
- `redact` (default for the secret rules) — mask only the offending span(s) and still
  serve the page; a legit response that leaks one key gets that key masked, not withheld
- `block` — withhold the whole response

Also fixes a real engine bug: array `parameter` (["get.a","post.b"]) — used pervasively in
rule_v2 — silently never matched (`parameter.indexOf('.')` on an array returns -1).
#evaluateCondition now resolves each element and ORs them.

Enforcement only in block mode; dry-run observes + serves original. Fail-open throughout.
createSupabaseGuard / createServerFnGuard unchanged. Full suite: 151 pass; build + typecheck green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderbuds

coderbuds Bot commented Jul 14, 2026

Copy link
Copy Markdown

Well-structured feature adding response redaction and egress SSRF with array-param support.

🎯 Quality: 100% Elite · 📦 Size: Large — consider splitting if possible

🛡️ 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 23rd PR — above team average · Averaging Excellent

See how your team is trending →

@patchstackdave

Copy link
Copy Markdown
Contributor Author

/review

@ejntaylor ejntaylor left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

node() consumes the request stream to screen it, then called next() without setting
req.body — so a downstream handler (or a body-parser mounted after the guard) saw an
empty stream. Now sets req.body to the parsed body on allow (only if not already set by
an upstream parser), matching the engine's createNodeMiddleware. Adds node-body tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@patchstackdave patchstackdave merged commit b3e9bdf into main Jul 14, 2026
4 checks passed
@patchstackdave patchstackdave deleted the feat/protect-tier3-response-egress branch July 14, 2026 10:13
patchstackdave added a commit that referenced this pull request Jul 14, 2026
Resolves the conflict from #57 (squash-merged): #57 modified tests/protect.test.ts while
this branch renamed it to tests/protect/runtime.test.ts — accepted the rename (its content
is preserved there). Picks up #57's node() req.body fix + node-body.test.ts from main, and
flips the now-obsolete "not re-exposed" note in runtime-guards to a real assertion.

Full suite 358 pass; typecheck green.

Co-Authored-By: Claude Fable 5 <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.

2 participants