Safeguard
Application Security•

CWE-352 Cross-Site Request Forgery: How It Works and How to Fix It

CSRF makes a logged in user's browser send a request they never intended. Learn how it works, a Flask example, and the defenses MITRE lists.

Safeguard Security Team
5 min read

CWE-352, Cross-Site Request Forgery (CSRF), is a web weakness. MITRE describes it as a web application that does not, or cannot, sufficiently verify whether a request was intentionally provided by the user who sent it. The request may have come from an unauthorized actor.

If your application lets a signed in user change something, such as an email address, a password, a payment or a setting, and it relies only on the session cookie to know who is asking, this applies to you.

How it happens

Browsers attach cookies to requests automatically. If you are signed in to a site, any request your browser sends to that site carries your session, no matter which page caused the request.

An attacker builds a page, or an image tag, or a hidden form, that points at the target site's state changing endpoint. The attacker sends a victim there, or places the content where the victim will see it. The victim's browser fires the request with the victim's cookie. The site sees a valid session and does the work.

MITRE's observed examples show how old and simple this is. CVE-2004-1703 and CVE-2004-1995 describe adding user accounts via a URL in an image tag. CVE-2004-1967 describes arbitrary code execution by specifying the code in a crafted image tag or URL.

An image tag works because loading an image is a GET request, and the application treated a GET as an action. That leads to the first rule below.

What an attacker gains

MITRE lists a wide set of consequences: gaining privileges or assuming identity, bypassing protection mechanisms, reading and modifying application data, and denial of service by crash, exit or restart. It adds that consequences vary with the functionality that is vulnerable. A forged request can only do what the victim could do, so an administrator victim is the worst case.

Note what the attacker does not get. They do not read the response. CSRF is about making things happen, not about stealing the reply.

How to find it

The MITRE record is candid about detection. Manual analysis, such as penetration testing and threat modeling, works. Automated static analysis does not do well: CSRF is hard to detect reliably with automated techniques, because each application has its own implicit security policy that decides which requests are acceptable. The record also lists bytecode weakness analysis for partial coverage.

Practical checks:

  • List every endpoint that changes state. Does each one require something an attacker's page cannot supply, such as a secret token?
  • Find any state change that accepts GET.
  • Test by submitting a state changing request from a different origin, or without the token. It must fail.
  • Review how your framework handles protection. Is it on by default, and is any endpoint exempt?

How to fix it

MITRE lists several defenses. Here they are in a usable order.

  1. Use a vetted library or framework that provides protection, and turn it on. MITRE mentions anti-CSRF packages such as OWASP CSRFGuard and the ESAPI session management component.
  2. Do not use GET for any request that changes state.
  3. Generate a unique, unpredictable token for each form or session, place it in the form, and verify it on receipt. MITRE stresses the token must not be predictable.
  4. For especially dangerous operations, send a separate confirmation request.
  5. The double submitted cookie method: set a pseudorandom value as a cookie and require the same value in the form. An attacker's page cannot read the cookie because of the same origin policy.
  6. Checking the Referer header can help, but MITRE warns it can break legitimate use when browsers or proxies strip it.

One more point from MITRE that deserves emphasis: make sure the application is free of cross-site scripting. Most CSRF defenses can be bypassed by attacker controlled script running in your own pages, so XSS undoes them.

A short Flask example. The vulnerable endpoint changes state on GET, with no token:

@app.get("/account/delete")
def delete_account():
    delete_user(session["uid"])       # an image tag can trigger this
    return "deleted"

The fixed version uses POST and verifies a per session token:

import hmac, secrets
from flask import abort, request, session

def csrf_token():
    if "csrf" not in session:
        session["csrf"] = secrets.token_urlsafe(32)
    return session["csrf"]

@app.post("/account/delete")
def delete_account():
    sent = request.form.get("csrf_token", "")
    if not hmac.compare_digest(sent, session.get("csrf", "")):
        abort(403)
    delete_user(session["uid"])
    return "deleted"

The form includes the token from the csrf_token function as a hidden field. A forged request from another site cannot know it. Most frameworks ship this as middleware, and using it beats writing it by hand.

Try it yourself

A dynamic scan can probe your forms for missing protection from the outside. The demo shows how a web scan walks an application and reports findings.

Click through it at your own pace, or open it full screen.

Quick check

Why is a state changing GET request dangerous?

Browsers load images and links automatically with the user's cookies. An image tag pointing at the endpoint will trigger the action, which is how the CVE-2004 examples added accounts.

Why does cross-site scripting weaken CSRF defenses?

Script running in your own page can read tokens and send requests as the user, so most CSRF defenses can be bypassed. Fix XSS as well.

Does a CSRF attacker see the response?

No. The attack triggers actions in the victim's session. It does not by itself read the reply.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.