diff --git a/DEPLOY.md b/DEPLOY.md index c34fdfd3..d1fc9af3 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -67,7 +67,7 @@ PAT instead), and Turnstile is only used by the planned Jira integration. **Verify configuration:** Run `pnpm validate:deploy` locally to check that all required Cloudflare Worker secrets are set. In CI, the deploy workflow runs -`pnpm validate:deploy --ci` automatically before building. +`pnpm validate:deploy` automatically before building. ### Optional (both deployment paths) @@ -230,7 +230,7 @@ Configure these rules in the Cloudflare dashboard under **Security → WAF**. **Where:** Security → WAF → Custom Rules **Expression:** ``` -(http.request.uri.path starts_with "/api/") and +(starts_with(http.request.uri.path, "/api/")) and not (any(http.request.headers["origin"][*] in {"https://YOUR-DOMAIN"})) and not (http.request.uri.path eq "/api/csp-report") and not (http.request.uri.path eq "/api/error-reporting") @@ -251,7 +251,7 @@ not (http.request.uri.path eq "/api/error-reporting") **Where:** Security → WAF → Rate Limiting Rules **Matching expression:** ``` -(http.request.uri.path starts_with "/api/") and +(starts_with(http.request.uri.path, "/api/")) and (http.request.method ne "OPTIONS") ``` **Rate:** 60 requests per 10 seconds per IP diff --git a/scripts/waf-smoke-test.sh b/scripts/waf-smoke-test.sh index 001b3bfa..ad7927a4 100755 --- a/scripts/waf-smoke-test.sh +++ b/scripts/waf-smoke-test.sh @@ -8,6 +8,7 @@ # Rules validated: # 1. Path Allowlist — blocks all paths except known SPA routes, /assets/*, /api/* # 2. Scanner User-Agents — challenges empty/malicious User-Agent strings +# 3. Origin Gate — blocks /api/* requests without valid Origin header # Rate limit rule exists but is not tested here (triggers a 10-minute IP block). set -euo pipefail @@ -58,9 +59,12 @@ TESTS=( "200|GET /privacy|${BASE}/privacy" "307|GET /index.html (html_handling redirect)|${BASE}/index.html" "200|GET /assets/nonexistent.js|${BASE}/assets/nonexistent.js" - "200|GET /api/health|${BASE}/api/health" - "400|POST /api/oauth/token (no body)|-X|POST|${BASE}/api/oauth/token" - "404|GET /api/nonexistent|${BASE}/api/nonexistent" + "200|GET /api/health (with Origin)|-H|Origin: ${BASE}|${BASE}/api/health" + "400|POST /api/oauth/token (no body)|-X|POST|-H|Origin: ${BASE}|${BASE}/api/oauth/token" + "404|GET /api/nonexistent|-H|Origin: ${BASE}|${BASE}/api/nonexistent" + # Rule 3: Origin gate — API requests without valid Origin are blocked at WAF + "403|GET /api/health (no Origin)|${BASE}/api/health" + "403|POST /api/oauth/token (wrong Origin)|-X|POST|-H|Origin: https://evil.example.com|${BASE}/api/oauth/token" # Rule 1: Path Allowlist — blocked paths "403|GET /wp-admin|${BASE}/wp-admin" "403|GET /wp-login.php|${BASE}/wp-login.php"