Skip to content

feat(protect): generic block message + branded HTML "Access Denied" page#78

Merged
patchstackdave merged 1 commit into
mainfrom
feat/protect-block-page
Jul 15, 2026
Merged

feat(protect): generic block message + branded HTML "Access Denied" page#78
patchstackdave merged 1 commit into
mainfrom
feat/protect-block-page

Conversation

@patchstackdave

Copy link
Copy Markdown
Contributor

Two client-facing changes to how a blocked request looks — less "WAF narrative", more like the Patchstack block view.

1. Generic block message (no WAF narrative)

The client body previously exposed "Blocked by Patchstack WAF rule: <title>" + the rule title. Now it's a generic "This request has been blocked by Patchstack." — no WAF wording, no rule title. The opaque rule id is kept as a machine field (server-fn receipts / support reference) and the full reason still goes to the server log via onDetect.

2. Branded HTML "Access Denied" page for navigations

When a blocked request is a top-level document navigation (Sec-Fetch-Dest: document, or falling back to Accept: text/html), the guard now serves a branded full-page view — ported from the Patchstack WordPress plugin's sample.php into src/protect/block-page.js (self-contained: inline CSS + base64 SVGs, the "secured by Patchstack" footer). XHR/fetch clients still get masked JSON.

  • URL is HTML-escaped (no reflected XSS).
  • Shows the opaque rule id as a reference code (like sample.php's "Error Code" row) — no rule detail / WAF narrative.
  • Wired across the fetch/route-WAF, node, and express block paths.

Tests

+4 (tests/protect/block-response.test.ts): HTML-vs-JSON routing by request type, generic message, opaque-id retention, URL escaping. 427 pass, typecheck + build clean.

🤖 Generated with Claude Code

Two client-facing changes to request blocks:

- De-narrativize the block text. The client body no longer says "Blocked by
  Patchstack WAF rule: <title>" — it's a generic "This request has been blocked
  by Patchstack." with no WAF wording and no rule title. The opaque rule id is
  kept as a machine field (server-fn receipts / support reference), and the full
  reason still goes to the server log via onDetect.
- Serve a branded full-page "Access Denied" view (ported from the Patchstack
  WordPress plugin's sample.php → src/protect/block-page.js, self-contained
  inline CSS + base64 SVGs) when a BLOCKED request is a top-level document
  navigation (Sec-Fetch-Dest: document, or Accept: text/html). XHR/fetch clients
  still get masked JSON. The URL is HTML-escaped (no reflected XSS); the page
  shows the opaque rule id as a reference code, no rule detail.

Wired across the fetch/route-WAF, node, and express block paths. +4 tests
(HTML vs JSON routing, generic message, XSS escaping). 427 tests.

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

coderbuds Bot commented Jul 15, 2026

Copy link
Copy Markdown

Branded HTML block page integrates well with robust JSON fallback.

🎯 Quality: 100% Elite · 📦 Size: Medium

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

See how your team is trending →

@patchstackdave

Copy link
Copy Markdown
Contributor Author

/review

@patchstackdave patchstackdave merged commit 64824ab into main Jul 15, 2026
4 checks passed
@patchstackdave patchstackdave deleted the feat/protect-block-page branch July 15, 2026 09:30
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