I'm trying to create a PoC for a CSRF issue I found. The request contains the CSRF token inside the request header and the problem is that the server side is checking only if the token is available without checking if it belongs to the current user session or not.

I tried to make a CSRF PoC with XmlHttpRequest by setting a custom header to override the CSRF token with one of my tokens (since the server doesn't validate the CSRF token), but this will be blocked due to CORS.

POST /endpoint
HEADER_CSRF: ANY_CSRF_TOKEN  <--- accept any other CSRF token 

Is there a way to achieve a CSRF PoC without being blocked due to CORS?

1 Answer 1


Just to check my understanding:

  • The site expects a custom header (even though it doesn't check the value)
  • The site doesn't allow you to pass the anti-CSRF token in any other way (e.g. body, URL, etc.)
  • The site either doesn't allow CORS at all, or its CORS policy does not allow third-party sites to set the header in question

Is that all correct? If so:

You have not found any CSRF issue; the site is not vulnerable to CSRF!

This is actually one of my favorite ways to mitigate CSRF.

  • It requires minimal logic: just check that a header exists, no need to do anything with it.
  • It requires no server-side state; the actual value is irrelevant.
  • It doesn't require any special configuration; the default CORS policy already enforces the behavior we want.
  • It does require client-side scripting (to add the header) on all state-changing requests to the server, but that's common these days (for e.g. bearer tokens or graphql) and unlike bearer tokens, the actual authentication token can be stored in an HttpOnly cookie, providing a small mitigation to XSS.
  • It's not especially fragile; the server would need to change its CORS configuration to allow third-party sites to make with-credentials requests and also set the header in question, and that's a lot of misconfigurations to do by accident.
  • It doesn't depend on new browser behaviors like the SameSite cookie flag; a browser that is too old for this to work will fail to be vulnerable because it's so old that it doesn't support CORS requests at all, which is both fail-safe and very old indeed.
  • It isn't vulnerable to attacks from subdomains, plain-text HTTP, or different ports the way SameSite often is; all of those are different origins and will require a CORS request, with pre-flight.

The pre-flight requirement is key, incidentally. Merely not allowing CORS requests DOES NOT prevent CSRF; the attacker can send a non-CORS cross-site request (either using the Fetch API, or just using an HTML form element that submits automatically) or a non-preflighted ("simple") CORS request, and even if the server responds with the CORS equivalent of "hey, that's not allowed!" the server might still also have processed the request normally. This happens even with credentialed (cookie-containing) requests; setting withCredentials=true doesn't make a CORS request require a pre-flight, and submitting a form automatically includes cookies. The only way to prevent the browser from sending the request is to reject requests that can be made without preflights.

With all that said, there's a few ways other than the custom header to do this:

  • You could require that the content type be something other than the types allowed for simple (non-preflighted) requests (plain text, url-encoded form, mime multipart). application/json is the usual pick, of course. Note that this means fully validating the Content-Type header value; it's not sufficient to merely require that the Content-Type header contain the string "json" (that can be spoofed in a simple request), and definitely not that the body merely be parsable as JSON.
  • You could require the the HTTP verb (method) be something other than the ones allowed for simple requests (GET, HEAD, POST).

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .