AuditCoreAuditCore
highCWE-352OWASP A01:2021 — Broken Access ControlCSRFXSRFCross-Site Request Forgery

How to fix Cross-Site Request Forgery (CSRF)

CSRF tricks a logged-in user's browser into making authenticated state-changing requests they didn't intend. Modern defenses are simpler than people think — SameSite=Lax cookies fix 95% of cases automatically. The remaining 5% need CSRF tokens. Here's the complete playbook.

What is CSRF (Cross-Site Request Forgery)?

CSRF works because browsers automatically attach cookies to every request to a domain — including requests originating from other origins. If user.com is logged into bank.com (session cookie set), and user.com renders `<form action='https://bank.com/transfer' method=POST><input name=amount value=10000><input name=to value=attacker></form><script>document.forms[0].submit()</script>`, the browser submits that form WITH the bank session cookie. Bank sees a valid authenticated request and processes the transfer.

The attack requires three things: (1) target is logged in (session cookie present), (2) target visits attacker page, (3) target's bank doesn't verify the request originated from its own forms. Defense is breaking link 3 — either by requiring an unguessable token in the request (CSRF tokens) or by configuring cookies to not be sent on cross-site requests (SameSite=Lax).

CSRF used to require complex form submissions, but `fetch()` made it easier and `application/json` made the old `Content-Type: form-urlencoded`-based defenses irrelevant. Modern CSRF lives in JSON APIs that use cookie auth — exactly the pattern most SPAs accidentally implement.

What an attacker can do

The concrete impact of leaving CSRF (Cross-Site Request Forgery) unpatched.

Funds transfer / state changes as the victim

Anything the user can do (change email, transfer money, delete account, post content), an attacker can trigger from a remote page.

Account takeover via email change

POST /settings/email with attacker's email → password reset to attacker → game over.

Worm propagation in social apps

Stored CSRF (XSS or DOM-based) auto-shares the attack page to the victim's contacts.

Privilege escalation in admin panels

Admin visits attacker's page, attacker triggers admin-only operations using admin session.

How do I know if I'm vulnerable?

Manual: check every state-changing endpoint (POST/PUT/PATCH/DELETE) for either (a) explicit CSRF token validation or (b) requirement of a non-cookie credential (Bearer header). If the only auth is session cookie and there's no token, you have CSRF.

Automated: AuditCore's authenticated crawler maps state-changing endpoints, then attempts to call them from a different origin (no cookies, no Referer to your domain). Endpoints that succeed without auth verification = CSRF. The Auth Replay scanner catches related issues (BFLA, missing role checks).

How to fix CSRF (Cross-Site Request Forgery)

5 ordered steps. Apply them in order — each builds on the previous.

  1. 1

    Set SameSite=Lax (or Strict) on session cookies

    SameSite=Lax tells the browser not to send the cookie on cross-site POST/PUT/DELETE requests. Top-level GET navigations still send it (so users clicking your-app links from elsewhere stay logged in).

    Chrome defaults to SameSite=Lax for cookies without explicit attribute since 2020. Firefox followed. Safari has stricter defaults. Set explicitly to ensure consistent behavior. Use Strict for non-shared apps where users never navigate from external links.

    javascriptStep 1
    // Express
    app.use(session({
      name: "sid",
      cookie: {
        httpOnly: true,
        secure: true,
        sameSite: "lax",  // or "strict"
        maxAge: 1000 * 60 * 60 * 24 * 7,
      },
    }));
    
    // Set-Cookie header equivalent:
    // Set-Cookie: sid=abc; HttpOnly; Secure; SameSite=Lax; Path=/
    
    // Django:
    # settings.py
    SESSION_COOKIE_SAMESITE = "Lax"
    SESSION_COOKIE_SECURE = True
    CSRF_COOKIE_SAMESITE = "Lax"
  2. 2

    Add CSRF tokens for forms (synchronizer pattern)

    Server generates a per-session token, embeds in form, validates on submission. Belt-and-suspenders alongside SameSite.

    Most frameworks have built-in CSRF middleware. Django's `{% csrf_token %}`, Rails' `csrf_meta_tags`, Express's `csurf` (deprecated but widely used; alternatives like `csrf-csrf` are maintained). Use them.

    pythonStep 2
    # Django — built-in
    # settings.py
    MIDDLEWARE = [..., 'django.middleware.csrf.CsrfViewMiddleware']
    
    # In template
    <form method="post">
      {% csrf_token %}
      <input name="email" value="{{ user.email }}">
      <button>Save</button>
    </form>
    
    # AJAX — read token from cookie + send in header
    fetch("/api/save", {
      method: "POST",
      headers: { "X-CSRFToken": getCookie("csrftoken") },
      body: JSON.stringify({...})
    });
  3. 3

    Use double-submit cookie pattern for stateless APIs

    When you can't keep server-side session state, the client reads a CSRF token from a cookie and sends it in a custom header. Browser never lets cross-origin pages read the cookie, so cross-origin requests can't include the matching header.

    Common pattern for SPAs. Token can be HMAC-signed for tamper resistance.

    javascriptStep 3
    // Server: set CSRF cookie on login + every authenticated response
    res.cookie("csrf-token", crypto.randomBytes(32).toString("hex"), {
      secure: true,
      sameSite: "lax",
      // NOT httpOnly — JS needs to read it
    });
    
    // Client: read cookie, send as header
    const csrfToken = document.cookie.match(/csrf-token=([^;]+)/)?.[1];
    fetch("/api/transfer", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-CSRF-Token": csrfToken,
      },
      body: JSON.stringify({ amount, to }),
      credentials: "include",
    });
    
    // Server middleware verifies header matches cookie
    function csrfCheck(req, res, next) {
      const headerToken = req.get("X-CSRF-Token");
      const cookieToken = req.cookies["csrf-token"];
      if (!headerToken || !cookieToken || headerToken !== cookieToken) {
        return res.sendStatus(403);
      }
      next();
    }
  4. 4

    Use Bearer token auth for APIs (not cookies)

    If your API uses Authorization: Bearer headers, CSRF is structurally impossible — cross-origin requests can't read or set arbitrary headers due to CORS preflight.

    Move from session cookies to JWT/OAuth Bearer tokens stored in memory (NOT localStorage — XSS reads it). Token gets sent on every API call manually. Caveat: lose CSRF immunity if you fall back to cookies for any endpoint.

    javascriptStep 4
    // Login returns Bearer token in response body
    const { token } = await fetch("/login", { method: "POST", body: ... }).then(r => r.json());
    
    // Store in memory (not localStorage — XSS hazard)
    let authToken = token;
    
    // Send on every API call
    const res = await fetch("/api/transfer", {
      headers: { "Authorization": `Bearer ${authToken}` },
      body: ...,
    });
    
    // Cross-origin pages CANNOT read your token. CSRF eliminated.
  5. 5

    Verify Origin/Referer headers as defense-in-depth

    Most state-changing requests should originate from your own domain. Reject requests where Origin doesn't match.

    Origin is more reliable than Referer (Referer can be stripped by privacy extensions). Don't reject when Origin is missing (legitimate cases: native apps, webhooks); rather, reject when it's PRESENT and doesn't match.

    javascriptStep 5
    function checkOrigin(req, res, next) {
      const origin = req.get("Origin");
      if (!origin) return next();  // legitimate cases
    
      const allowed = ["https://app.example.com", "https://example.com"];
      if (!allowed.includes(origin)) {
        return res.status(403).json({ error: "Cross-origin request blocked" });
      }
      next();
    }
    
    app.use(["/api/transfer", "/api/admin"], checkOrigin);

How to verify the fix

Run AuditCore — its authenticated crawler maps state-changing endpoints, then attempts to call them with no cookies and from a different Origin. Findings include which endpoints accept cross-origin requests.

Manual: open your browser's dev tools, go to a different domain (e.g., example.org). In console: `await fetch('https://your-app.com/api/changeEmail', { method: 'POST', body: JSON.stringify({email:'[email protected]'}), headers: {'Content-Type': 'application/json'}, credentials: 'include' })`. If your endpoint processes this, you have CSRF.

FAQ

Frequently asked questions

Is CSRF still relevant in 2026 with SameSite=Lax default?+

Yes. SameSite=Lax handles top-level POST/PUT/DELETE from cross-origin pages, but doesn't handle: subdomain confusion (attacker controls a subdomain), GET-based state changes (legacy APIs that mutate on GET), or apps that explicitly set SameSite=None for embedded use cases. Defense-in-depth with tokens still recommended.

Should I use CSRF tokens AND SameSite cookies?+

Yes — they protect against different scenarios. SameSite catches most cross-site requests at the browser level. Tokens catch the cases SameSite misses (subdomains, certain navigation patterns) and provide a clear audit trail (server-side logs show the validation failure).

Does using JSON APIs (not form-urlencoded) protect against CSRF?+

Used to (because cross-origin form posts couldn't send Content-Type: application/json without CORS preflight). Modern fetch() can include arbitrary content types if the server allows the preflight. If your server has permissive CORS, JSON content-type is no longer a CSRF defense.

What about CSRF in mobile apps?+

Mobile apps don't use cookies (they use Bearer tokens), so CSRF doesn't apply. The CSRF threat model is browser-specific because browsers auto-attach cookies. Native apps don't have that auto-attach behavior.

Does my GraphQL API need CSRF protection?+

If it accepts cookies for auth: yes, same rules apply. If it requires Authorization header: no, CSRF doesn't apply because cross-origin pages can't add arbitrary headers without preflight + CORS approval. Most modern GraphQL implementations use Bearer tokens precisely to avoid CSRF.

Don't just guess — scan and verify

AuditCore Free Trial scans your homepage for CSRF (Cross-Site Request Forgery) and 50+ other vulnerability classes. No credit card. Results in 60 seconds.