ASVS 5.0 requirement V3.5.2 says that if your application relies on CORS preflight to stop disallowed cross-origin use of sensitive functionality, it must be impossible to call that functionality with a request that does not trigger a preflight. That may mean checking the Origin and Content-Type headers, or requiring an extra header that is not CORS-safelisted. It is a Level 1 requirement.
Why it matters
Preflight is an OPTIONS request the browser sends before certain cross-origin requests, asking whether the real request is allowed. It is only sent for non-simple requests: methods other than GET, HEAD and POST, custom headers, or content types other than application/x-www-form-urlencoded, multipart/form-data and text/plain.
Here is the trap. A JSON API expects Content-Type: application/json, which triggers a preflight. But if the server parses the body no matter what the content type says, an attacker's page can send the same JSON as text/plain in a plain fetch or form post. No preflight is sent, the request carries the victim's cookies, and the action runs.
How to implement it
Enforce the content type strictly, and fail anything else:
app.use("/api", (req, res, next) => {
if (["POST", "PUT", "PATCH"].includes(req.method) && !req.is("application/json")) {
return res.status(415).json({ error: "content type must be application/json" });
}
next();
});
app.use(express.json({ type: "application/json" }));
Flask:
@app.before_request
def require_json():
if request.method in ("POST", "PUT", "PATCH") and request.mimetype != "application/json":
abort(415)
Add a second guard: require a custom header, for example X-Requested-With, and validate the Origin header against your allowlist for state changing requests. Do not parse form encoded bodies on JSON endpoints at all, because frameworks that merge them make the bypass easy.
How to test it
Replay a sensitive JSON request with a simple content type:
curl -si -X POST https://api.example.com/v1/transfer -b cookies.txt \
-H "Content-Type: text/plain" -H "Origin: https://evil.example" \
-d '{"to":"acct2","amount":1}'
curl -si -X POST https://api.example.com/v1/transfer -b cookies.txt \
-H "Content-Type: application/x-www-form-urlencoded" -d "to=acct2&amount=1"
Both must be rejected with 415 or 403, and no transfer made. Also test without any content type and with a charset suffix. Then send a request from a foreign Origin without the custom header and confirm it is refused.
Common mistakes
Parsing any body as JSON. Some middleware ignores the declared type.
Allowing GET for state changes. GET never preflights.
Trusting CORS headers alone. They govern reads, not whether the request executes.
Quick check
Which content types avoid preflight?
application/x-www-form-urlencoded, multipart/form-data and text/plain. Anything else, such as application/json, triggers one.
Sources
- OWASP ASVS 5.0, requirement V3.5.2
- MDN: Preflight request