Cloudflare WAF Returning 403 — Why It Looks Like a CORS Error
Updated April 2026
Reading this? Verify your fix in real-time. Test your CORS config → CORSFixer
Your CORS config is correct. The request fails anyway. Cloudflare's WAF is intercepting the OPTIONS preflight and returning 403 before your server ever sees the request. The browser reports it as a CORS error because the 403 has no CORS headers.
Diagnose — is it WAF or is it CORS?
# Step 1: Check the status code of the OPTIONS request in DevTools # Network tab → find the OPTIONS request → Status column # Status 0 or no ACAO header = genuine CORS config problem # Status 403 = WAF or server rejecting the request # Step 2: Check Cloudflare dashboard # Security → Events → filter by your domain and time range # Look for blocked requests matching your API endpoint URL
Find which WAF rule is firing
# The Cloudflare Security Events log shows: # - Rule ID that matched # - The matched value # - Action taken (block, challenge, log) # Common rules that block legitimate API requests: # 100001 — SQL Injection detection (fires on Base64 in Authorization header) # 100015 — XSS detection (fires on certain JSON payloads) # Managed rules from OWASP Core Rule Set
Fix 1 — Create a WAF Skip Rule for your API
# Cloudflare Dashboard: # Security → WAF → Create rule → Skip # Rule name: Allow API CORS preflight # When: URI Path contains /api/ AND Request Method equals OPTIONS # Then: Skip WAF managed rules # Or allow your specific origin: # When: http.request.headers["origin"] contains "yourapp.com" # Then: Skip WAF managed rules
Fix 2 — Allowlist specific origins in WAF
# Firewall Rules (or WAF Custom Rules):
# Rule: Allow known origins
# Expression: (http.request.headers["origin"] in {"https://yourapp.com" "https://staging.yourapp.com"})
# Action: Skip all managed rules
# This is safer than skipping for all /api/ requests
Fix 3 — Handle at Cloudflare Worker (bypass WAF for OPTIONS)
// worker.js — intercept OPTIONS before WAF applies
export default { async fetch(request, env, ctx) { // Return CORS preflight response immediately // Cloudflare Workers run before WAF on incoming requests if (request.method === "OPTIONS") { const origin = request.headers.get("Origin"); const ALLOWED = ["https://yourapp.com"]; if (ALLOWED.includes(origin)) { return new Response(null, { status: 204, headers: { "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", "Access-Control-Allow-Headers": "Authorization, Content-Type", "Access-Control-Max-Age": "86400", } }); } } return fetch(request); // pass through to origin + WAF for everything else }
}
The diagnostic curl command
# Test the OPTIONS request directly — bypass browser curl -X OPTIONS https://api.yourapp.com/endpoint -H "Origin: https://yourapp.com" -H "Access-Control-Request-Method: POST" -H "Authorization: Bearer your-token" -v 2>&1 | grep -E "< HTTP|< access-control|cf-ray|< x-" # If you see HTTP/2 403 — WAF is blocking # Look for cf-ray header to reference in Cloudflare logsTest your CORS config → CORSFixer