Skip to content

CWE-352: Cross-Site Request Forgery (CSRF)

Overview

Cross-Site Request Forgery (CSRF) is an attack that forces authenticated users to perform unwanted actions on a web application. The attacker gets the victim's browser to issue the request, so it arrives carrying that user's session cookie and the application processes it as if the user had intended it. Unlike CWE-79 (Cross-Site Scripting), which exploits a user's trust in a website, CSRF exploits a website's trust in the user's browser.

Relationship to Other CWEs

Report a finding here when the defect is that nothing verified where an authenticated, state-changing request came from. CSRF is not an ordinary weakness entry: several weaknesses have to be present at once before the attack works, and MITRE calls it "attack-oriented in nature", asking you to "perform root-cause analysis to determine if other weaknesses allow CSRF attacks to occur, and map to those weaknesses" - predictable CSRF tokens being its own example of a finding that belongs with the randomness weaknesses instead. So where a protection is present and defeated by some other flaw, report that flaw.

The neighbours differ by which part of the forged request they name:

  • CWE-352 (this page) - the whole composite: a state-changing request arrives from a site the attacker controls, carries the victim's ambient session credentials, and is acted on
  • CWE-346 (Origin Validation Error) - the missing check on its own, in its general form. CORS misconfiguration and Referer trust live there; if the finding is CSRF itself, the deeper remediation is here
  • CWE-441 (Unintended Proxy or Intermediary ('Confused Deputy')) - the shape of the attack rather than the missing check. In CSRF the confused deputy is the victim's browser and the borrowed authority is a cookie the browser attaches on its own
  • CWE-642 (External Control of Critical State Data) - the third component MITRE names: the parameters of the state change are chosen by the attacker's page, not by the user the request appears to come from
  • CWE-79 (Cross-Site Scripting (XSS)) - MITRE's peer entry, and the mirror image of this one: XSS abuses the user's trust in the site, CSRF the site's trust in the user's browser. MITRE also notes they chain, with CSRF used to submit a request carrying an XSS payload. Script running on your own origin can read any token, so XSS anywhere on the site defeats every defense on this page
  • CWE-338 (Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)) - where a guessable token belongs. The synchronizer token below is only as good as the generator behind it, and a token from Math.random() or a seeded Random is forgeable while the CSRF protection appears to be in place

OWASP Classification

A01:2025 - Broken Access Control

Risk

High: CSRF vulnerabilities let attackers perform unauthorized actions on behalf of authenticated users:

  • Account takeover: Change email, password, or security settings
  • Financial fraud: Transfer funds, make purchases, change payment methods
  • Data manipulation: Create, modify, or delete user data
  • Privilege escalation: Add administrative users, change permissions
  • System compromise: Execute administrative functions with user credentials

The impact is directly tied to the victim's privileges - CSRF against an administrator can compromise the entire application.

Remediation Steps

Core Principle: Never allow authenticated, state-changing requests to be processed unless their origin and authenticity are verified using server-controlled mechanisms.

Identify state-changing endpoints lacking CSRF protection

  • Start from the file, line, and endpoint in the finding, then look for the same gap elsewhere: every POST, PUT, DELETE, or other operation that modifies data needs protection
  • Note which of those endpoints rely on the session cookie alone for authentication
  • Find forms and AJAX requests that send no CSRF token
  • Check whether the framework's CSRF protection has been disabled in configuration

Use Synchronizer Token Pattern (Primary Defense)

  • Generate a cryptographically random token of at least 32 bytes for each user session and store it server-side in the session
  • Include the token in every form as a hidden field, <input type="hidden" name="csrf_token" value="...">, and in AJAX requests as a custom header such as X-CSRF-Token: ...
  • Validate the token on the server for every state-changing request, and return 403 Forbidden when it is missing or invalid
  • Prefer the framework's own CSRF protection or a maintained library: Django, Rails, Spring Security, maintained Express/Fastify middleware

Why this works: the same-origin policy stops an attacker's page from reading the token, so a forged request cannot carry a valid one and fails validation.

  • Set SameSite=Strict or SameSite=Lax on all session cookies
  • SameSite=Strict gives the strongest browser-enforced isolation: the cookie is sent only on same-site requests, which can break legitimate cross-site entry flows
  • SameSite=Lax allows top-level navigation from external sites; use it when users need to arrive at the application from elsewhere
  • Always combine with Secure and HttpOnly, so the cookie is sent only over HTTPS and is not readable from JavaScript
  • Example: Set-Cookie: sessionid=...; SameSite=Strict; Secure; HttpOnly
  • SameSite is defense in depth, not the only protection for authenticated state-changing endpoints; keep server-side tokens or equivalent request validation

Apply Origin and Referer header validation

  • Check that the Origin header matches your application's domain on every state-changing request; if Origin is absent, fall back to Referer
  • Compare against an allowlist of your own domains and reject anything else
  • Decide deliberately how to treat a request with neither header: privacy controls, older clients, and same-origin form submissions can all affect whether the headers arrive
  • Perform the check over HTTPS so the headers cannot be tampered with in transit
  • Treat this as an additional layer, not the primary protection

Add re-authentication for critical operations

  • For sensitive actions such as password changes, fund transfers, and account deletion, require the user to re-enter their password
  • Use step-up authentication for high-risk operations: a confirmation step such as email confirmation or 2FA before the transaction completes
  • Add delays or rate limiting for sensitive operations
  • Log all critical actions with full context: IP, user agent, timestamp

Test and verify CSRF protection

  • Requests with no token, an invalid token, or a token issued to a different session are rejected
  • A cross-site test page that submits a forged POST fails (see Testing and Verification below)
  • GET requests cannot perform state changes
  • SameSite cookies are withheld on cross-site requests in the browser, and Origin/Referer validation blocks external requests
  • AJAX requests send the token in a header, and the framework's protection is enabled and applied to every state-changing endpoint
  • Legitimate forms and AJAX requests still work
  • Re-scan with the security scanner to confirm the issue is resolved and that the changes introduced no new findings

Common Pitfalls

  • Token present but not bound to the session: Generating a random token and checking only that the submitted value "looks like" a token, rather than comparing it against the value stored server-side for that authenticated session - an attacker can obtain a valid token from their own session (or an anonymous one) and replay it in the forged request.
  • Protecting POST but missing a state-changing GET or legacy route: CSRF middleware enabled for the main form handlers, while an older or convenience endpoint that mutates state via GET (GET /account/delete?confirm=true) is skipped, since CSRF protection conventionally guards only non-safe HTTP methods - an attacker only needs an <img> tag or link to trigger it.
  • Treating SameSite cookies as sufficient on their own: Relying solely on SameSite=Lax/Strict without a server-side token. Top-level GET navigations still carry Lax cookies, and any sibling subdomain the browser considers "same site" (or one compromised by XSS) can still originate requests that bypass the protection.
  • New endpoint added outside the protected code path: CSRF validation wired into the original form handler, but a later JSON/API endpoint added for the same underlying action isn't routed through the same middleware or filter - the original flow is protected, the newer parallel one is not.

Testing and Verification

Manual CSRF Testing

<!-- Create a test page on attacker.com -->
<form action="https://vulnerable-app.com/transfer" method="POST">
  <input type="hidden" name="to_account" value="attacker">
  <input type="hidden" name="amount" value="1000">
</form>
<script>document.forms[0].submit();</script>

Expected result: The request is rejected because of:

  • Missing CSRF token
  • SameSite cookie blocking in browsers that apply the attribute
  • Origin/Referer validation failure

Test Token Validation

  • Submit a request without a token: should fail
  • Submit a request with an invalid token: should fail
  • Reuse a token from a different session: should fail
  • Submit a GET request for a state change: should fail

Verify session cookies have proper attributes:

Set-Cookie: session=...; SameSite=Strict; Secure; HttpOnly; Path=/

Test Framework Protection

  • Framework CSRF protection is enabled
  • Protection applies to all state-changing endpoints
  • AJAX requests include CSRF tokens
  • Error handling doesn't leak information

Migration Considerations

Adding CSRF protection can break existing integrations and AJAX requests.

What Breaks

  • AJAX requests that do not send a CSRF token are rejected
  • Third-party integrations and mobile apps that call your endpoints stop working
  • Old HTML forms without CSRF tokens stop working
  • Automated scripts using curl or wget need to include tokens
  • Testing tools such as Postman and automated tests need updated headers

Migration Approach

Roll the protection out gradually:

  1. Log CSRF violations without blocking

    • Record failed CSRF validations without rejecting the request
    • Monitor logs to identify affected endpoints
  2. Add tokens to all forms and AJAX requests

    • Update all HTML forms to include CSRF tokens
    • Configure AJAX libraries to include tokens in headers
    • Test that all functionality works with tokens
  3. Enable enforcement

    • Configure the application to reject requests without valid tokens
    • Monitor error rates and user reports
    • Be prepared to add exemptions quickly if needed
  4. Monitor and adjust the allowlist

    • Review logs for legitimate failures
    • Add API endpoint exemptions as needed
    • Remove temporary exemptions once the migration is complete

Exempt endpoints sparingly. For legitimate API endpoints that can't use CSRF tokens:

  • Only exempt endpoints using alternative authentication such as API keys or OAuth
  • Never exempt endpoints relying solely on session cookies
  • Document all exemptions and their justification
  • Review the exemption list regularly

Rollback Procedures

If CSRF protection breaks functionality:

  1. Revert to logging mode behind a feature flag
  2. Allowlist the problematic endpoints temporarily
  3. Keep logging-only mode running longer to identify all issues

Testing Recommendations

State each as an assertion with the result you expect, so a pass means something:

  • A forged cross-site POST with a valid session cookie and no token is rejected with 403, and the action did not occur - check the data, not just the status.
  • A token issued to one session, replayed in another, is rejected. This is the one that catches a token generated correctly but never bound to a session.
  • Every form and AJAX path in the application still succeeds with a token present. Enabling CSRF protection breaks legitimate traffic far more often than it fails to block an attack, and only this test sees that.
  • API clients using an alternative authentication mechanism still authenticate, and endpoints relying on session cookies alone are not on the exemption list.
  • Under concurrent load with multiple workers, tokens issued by one worker validate on another. A secret generated at process start passes every single-process test and fails here.

Language-Specific Guidance

Concrete APIs and framework examples for each stack:

Additional Resources