ASVS 5.0 requirement V3.5.1 says that if your application does not rely on the CORS preflight mechanism to stop disallowed cross-origin requests to sensitive functionality, then those requests must be validated as coming from the application itself. You can use anti-forgery tokens, or require an extra header that is not CORS-safelisted. This is the defense against cross-site request forgery. It is a Level 1 requirement.
Why it matters
Browsers attach cookies to requests automatically, whichever site triggered them. A hostile page can submit a hidden form to https://bank.example/transfer and the browser will send the victim's session cookie along. The server sees a valid session and performs the action. See CWE-352.
CORS does not stop this on its own. A plain form post is a "simple request" and never triggers a preflight, so the request reaches your server and runs. CORS only limits whether the attacker's page can read the reply, not whether the action happens.
SameSite cookies help, and Lax is now the browser default, but it is a second layer. It does not cover same site attacks from a sibling subdomain, and some flows need SameSite=None.
How to implement it
Pick one of two approaches, or use both.
Anti-forgery token. The server issues an unpredictable token tied to the session, the page sends it in a hidden field or header, and the server rejects requests without a match. Most frameworks have this built in.
Django:
MIDDLEWARE = [
# ...
"django.middleware.csrf.CsrfViewMiddleware",
]
<form method="post">{% csrf_token %}...</form>
Spring Security keeps CSRF protection on by default for browser sessions. Do not call http.csrf().disable() unless the service is purely token authenticated with no cookies.
Express with csrf-csrf (double submit) follows the same idea: verify a token in X-CSRF-Token against a signed cookie value.
Custom header. Browsers cannot add a non-safelisted header such as X-Requested-With or X-CSRF to a cross-origin request without a preflight, and your CORS policy refuses that preflight. Require the header on every state changing route:
function requireCsrfHeader(req, res, next) {
if (["POST", "PUT", "PATCH", "DELETE"].includes(req.method) &&
req.get("X-Requested-With") !== "app") {
return res.status(403).json({ error: "missing header" });
}
next();
}
app.use(requireCsrfHeader);
This works only when your CORS configuration does not allow arbitrary origins and headers, see V3.4.2, and V3.5.2 covers the content type pitfall.
Also, keep SameSite=Lax or Strict on session cookies as a backstop.
How to test it
Log in, capture a state changing request in your proxy, and replay it three ways:
# 1. no token
curl -si -X POST https://app.example.com/settings/email -b cookies.txt -d "email=a@b.test"
# 2. wrong token
curl -si -X POST https://app.example.com/settings/email -b cookies.txt \
-H "X-CSRF-Token: wrong" -d "email=a@b.test"
# 3. foreign origin
curl -si -X POST https://app.example.com/settings/email -b cookies.txt \
-H "Origin: https://evil.example" -d "email=a@b.test"
All three must return 403 or equivalent, and the email must not change. Then host a small HTML page on another origin with a form posting to the endpoint and submit it from a logged in browser. It must fail.
Test every unsafe method and every route, including JSON endpoints, file uploads and logout.
Common mistakes
Disabling CSRF globally to fix an API error. Scope the exemption to token authenticated routes.
Checking only for token presence. Compare the value to the session.
Token in a cookie only. The server must compare against something the attacker cannot set.
Missed methods. Some frameworks protect POST but allow state change on GET or PUT.
Trusting Referer or Origin alone when absent. If the header is missing, reject rather than allow.
Quick check
Does SameSite=Lax make tokens unnecessary?
It is a useful layer but not a replacement. Same site attackers, older browsers and special flows can still bypass it.
Is a bearer token in an Authorization header exposed to CSRF?
No, since browsers do not attach it automatically. The risk comes from cookie based authentication.
Sources
- OWASP ASVS 5.0, requirement V3.5.1
- OWASP CSRF Prevention Cheat Sheet
- CWE-352 on Gold