Agent Wranglers

APS-18Required

Block cross-site form posts

Make sure a change of state only comes from your own pages, with SameSite cookies plus a token or an origin check.

How to do it

Never change anything on a GET request. For cookie logins, check Sec-Fetch-Site on every POST, PUT, PATCH and DELETE, and fall back to the Origin header. Refuse when neither proves the request came from your own origin. Or add a token to each form. SameSite alone does not stop attacks from your own subdomains. APIs that take a bearer token in a header are not exposed. The website rules refuse odd requests from other sites and set cookies safely add two more layers.

Examples

JavaScript example

const UNSAFE = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
app.use((req, res, next) => {
  if (!UNSAFE.has(req.method)) return next();
  const site = req.get('sec-fetch-site');
  const sameOrigin = site ? site === 'same-origin' : req.get('origin') === 'https://example.com';
  return sameOrigin ? next() : res.sendStatus(403);
});

Python example

# Python with Flask
UNSAFE = {"POST", "PUT", "PATCH", "DELETE"}

@app.before_request
def refuse_cross_site():
    if request.method not in UNSAFE:
        return
    site = request.headers.get("Sec-Fetch-Site")
    same_origin = site == "same-origin" if site else request.headers.get("Origin") == "https://example.com"
    if not same_origin:
        abort(403)

How to check

Send a POST to a change-of-state route with the test user's cookie and Origin: https://evil.example.net, then with Sec-Fetch-Site: cross-site. The server refuses both with 403. The same POST with your own origin still works.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/application-security/authentication-and-sessions/cross-site-requests">Agent Wranglers application security playbook</a>. In code you write, add a comment next to it with the same link.