Files
agent-skills/software-development/csp-form-action-debugging/SKILL.md
Hermes Agent ccc63d1e70 first commit
2026-05-10 13:52:46 +08:00

3.6 KiB

name, description, tags, triggers
name description tags triggers
csp-form-action-debugging Debug CSP form-action blocking issues — Chrome silently blocks forms when action URLs contain certain patterns in query params.
csp
security
debugging
form-action
chrome
form submission fails silently
CSP form-action violation
login redirect not working
form POST blocked by browser
ERR_ABORTED on form submit

CSP form-action Debugging

The Core Problem

Chrome's CSP form-action 'self' implementation has a known behavior: when a form's action URL contains // in the query string (e.g., ?redirect=https%3A//example.com/), the CSP evaluator may decode %3A: internally, interpret :// as a scheme separator, and block the submission as cross-origin — even though the action URL itself is same-origin.

Symptom: Form POST returns net::ERR_ABORTED in DevTools network tab. Cookie may still be set (server responded), but browser doesn't follow the redirect.

Console error:

Sending form data to 'https://same-origin.example/api?redirect=https%3A//other.example/' 
violates the following Content Security Policy directive: "form-action 'self'". 
The request has been blocked.

Diagnosis Steps

  1. Check CSP headers on the page:

    curl -sI "https://page-url" | grep -i content-security-policy
    

    Look for form-action directive.

  2. Check form action — is it a clean URL or does it carry redirect/return params in query string?

    document.querySelector('form').action
    
  3. Test without query params — if form works without the redirect param in action URL, CSP is the culprit.

  4. Verify with Playwright console listener:

    page.on("console", lambda msg: print(f"[{msg.type}] {msg.text}"))
    # CSP violations appear as [error] messages
    

Fix Pattern

Move redirect/return_url from query string to hidden form field:

Before (broken):

<form method="POST" action="/api/login?redirect=https%3A//example.com/">

After (fixed):

<form method="POST" action="/api/login">
  <input type="hidden" name="redirect" value="https://example.com/">

Backend change — read from Form body instead of Query params:

# Before
async def login(redirect: str | None = Query(default=None)):
# After  
async def login(redirect: str | None = Form(default=None)):

Why This Happens

Chrome's CSP parser normalizes URLs before checking against form-action. The normalization decodes percent-encoded characters in the query string, turning %3A// into ://. The parser then treats the substring after :// as a different-origin host.

This does NOT affect:

  • Relative paths in query params (?redirect=/dashboard)
  • Same-origin absolute URLs in query params (?redirect=https://same-origin.example/page)
  • Cross-origin URLs that don't contain // (rare)

It DOES affect:

  • Cross-origin URLs with // in query params (the common case for redirect parameters)

Pitfalls

  • Don't just remove form-action from CSP — it's a valuable XSS mitigation. The hidden field fix is better.
  • redirect: 'manual' in fetch shows the response as opaqueredirect with status 0 — this confirms the server IS responding correctly; the block is client-side.
  • Rate limiting can confuse diagnosis — if you're testing login repeatedly, 429 responses may appear alongside CSP errors. Wait 60s between tests or use fresh browser contexts.
  • This affects ALL forms, not just login. Any form that passes URLs in query parameters (e.g., ?return_to=, ?next=, ?callback=) is vulnerable.