Skip to content
Fast-turnaround security assessments available — 10+ years development & security experienceGet started
vulnerabilityCWE-352OWASP A01:2021Typical severity: High

Cross-Site Request Forgery: Forcing Actions on Behalf of Authenticated Users

·10 min read

Cross-Site Request Forgery: Forcing Actions on Behalf of Authenticated Users

Every time you visit a website where you are already logged in, your browser silently attaches your session cookies to every request sent to that domain. This is what keeps you authenticated as you navigate between pages. It is also what makes cross-site request forgery possible.

CSRF exploits a fundamental behavior of the web: the browser does not distinguish between requests that the user intentionally initiates and requests that a malicious page triggers in the background. If a user is logged into their banking application and visits a page controlled by an attacker, that page can submit a form to the banking application. The browser attaches the session cookie. The bank processes the transfer. The user never clicked a button.

This vulnerability sits under the Broken Access Control category in the OWASP Top 10 (A01:2021), and despite being well-understood, it continues to appear in production applications that rely on cookies for session management without implementing proper request verification.

How CSRF Attacks Work

The mechanics of a CSRF attack are straightforward. The attacker needs three conditions to be true:

  1. The victim is authenticated to the target application (they have an active session cookie).
  2. The target application relies solely on cookies (or other automatically-included credentials) to identify the user.
  3. The attacker can predict the request structure — the URL, method, and parameters needed to perform the action.

If all three conditions are met, the attacker crafts a request and delivers it through a channel that the victim will encounter.

A Basic CSRF Attack

Consider a web application where changing the user's email address is a simple POST request:

http
POST /account/email HTTP/1.1
Host: app.example.com
Cookie: session=abc123def456
Content-Type: application/x-www-form-urlencoded
 
email=user@legitimate.com

There is no CSRF token. No secondary verification. Just the session cookie and the new email value.

The attacker creates a page containing a hidden auto-submitting form:

html
<html>
<body onload="document.getElementById('csrfForm').submit()">
  <form id="csrfForm" action="https://app.example.com/account/email" method="POST">
    <input type="hidden" name="email" value="attacker@evil.com" />
  </form>
</body>
</html>

When the victim visits this page — whether through a link in a forum post, an embedded iframe, or a phishing email — the form submits automatically. The browser includes the victim's session cookie because the request is going to app.example.com, where the victim is authenticated. The server changes the email address to the attacker's.

From there, the attacker triggers a password reset to their new email address and takes over the account entirely.

GET-Based CSRF

Some applications perform state-changing actions via GET requests, which makes CSRF even simpler. No form submission is required — an image tag is enough:

html
<img src="https://app.example.com/account/delete?confirm=yes" width="0" height="0" />

The browser loads the "image" by sending a GET request with the victim's cookies. The server processes the account deletion. The victim sees a broken image icon, if they notice anything at all.

This is why the HTTP specification states that GET requests should be safe and idempotent — they should never cause side effects. When developers ignore this principle, they create trivially exploitable CSRF conditions.

JSON-Based CSRF

Modern applications often use JSON request bodies, which might seem immune because Content-Type: application/json cannot be set through a simple HTML form. However, several bypass techniques exist.

If the server does not strictly validate the Content-Type header, a form submission with enctype="text/plain" can deliver a payload that the server parses as JSON:

html
<form action="https://app.example.com/api/transfer" method="POST" enctype="text/plain">
  <input name='{"amount":10000,"to":"attacker-account","ignore":"' value='"}' />
</form>

This produces a request body of {"amount":10000,"to":"attacker-account","ignore":"="} — valid JSON if the server is lenient with parsing.

If the server does enforce Content-Type checking, the attacker might use a Flash-based redirect (on legacy systems) or exploit CORS misconfigurations that allow cross-origin requests with custom headers.

Real-World Impact

Account Takeover via Email Change

During a security assessment of a financial services platform, testers discovered that the account email change endpoint accepted POST requests authenticated solely by the session cookie. No CSRF token was required, and no confirmation email was sent to the original address.

The attack chain was devastating in its simplicity: forge a request to change the victim's email, then use the platform's password reset flow to set a new password. The entire account takeover could be triggered by a single page visit — no clicks required. The platform processed over fifty thousand financial transactions daily, and every authenticated user was vulnerable.

Administrative Action Forgery

A content management system used by a large media organization had CSRF vulnerabilities on its administrative endpoints. An attacker could craft a page that, when visited by a logged-in administrator, would create a new admin account with attacker-controlled credentials.

The attack was delivered through a comment on the public-facing website. A moderator reviewing the comment triggered the CSRF payload, which silently created a backdoor admin account. The attacker then used that account to modify published content, inject redirects, and access the subscriber database.

A regional banking platform implemented its internal transfer feature as a simple form submission protected only by session cookies. Testers demonstrated that a malicious page could initiate transfers between the victim's own accounts — or to external accounts if the recipient was pre-registered. The bank's lack of transaction signing or step-up authentication meant that the CSRF vulnerability was equivalent to direct access to the victim's funds.

Why Some Defenses Fail

Several commonly attempted defenses against CSRF are insufficient on their own:

Checking the Referer header: The Referer header indicates where a request originated, and some applications reject requests with foreign Referers. However, Referer can be stripped entirely using <meta name="referrer" content="no-referrer"> or certain redirect techniques. If the server allows requests with no Referer (which many do, to avoid breaking legitimate traffic), the defense is bypassed.

Requiring POST instead of GET: This stops image-tag attacks but not form-based attacks. A hidden auto-submitting form sends POST requests just as easily as GET.

Using unpredictable URLs: Obscuring endpoint paths does not help if the attacker can discover them through documentation, JavaScript source code, or by having their own account on the platform.

CORS configuration: CORS controls which origins can read responses, but it does not prevent the browser from sending the request in the first place. A CSRF attack does not need to read the response — it just needs the server to process the request.

Prevention Strategies

Anti-CSRF Tokens (Synchronizer Token Pattern)

The most widely deployed defense is the synchronizer token pattern. The server generates a unique, unpredictable token for each user session and embeds it in every form and state-changing request.

Server-side token generation:

python
import secrets
 
def generate_csrf_token(session):
    if 'csrf_token' not in session:
        session['csrf_token'] = secrets.token_hex(32)
    return session['csrf_token']

The token is included in forms as a hidden field:

html
<form action="/account/email" method="POST">
  <input type="hidden" name="csrf_token" value="a1b2c3d4e5f6..." />
  <input type="email" name="email" value="" />
  <button type="submit">Update Email</button>
</form>

On the server, every state-changing request is validated:

python
def validate_csrf(request, session):
    token = request.form.get('csrf_token')
    if not token or token != session.get('csrf_token'):
        abort(403, 'CSRF validation failed')

The attacker cannot read the token from the page (blocked by the same-origin policy), so they cannot include it in their forged request. Without a valid token, the server rejects the request.

For single-page applications that communicate via APIs, the token is typically sent in a custom HTTP header (such as X-CSRF-Token), which cannot be set by cross-origin form submissions.

An alternative approach that does not require server-side state. The server sets a random value in both a cookie and as a request parameter. On each request, the server verifies that the cookie value matches the parameter value.

Set-Cookie: csrf=randomValue123; Path=/; Secure; SameSite=Strict

The client reads the cookie value via JavaScript and includes it as a header:

javascript
fetch('/api/transfer', {
    method: 'POST',
    headers: {
        'X-CSRF-Token': getCookie('csrf')
    },
    body: JSON.stringify({ amount: 500, to: 'recipient' })
});

This works because the attacker can cause the browser to send the cookie but cannot read its value (same-origin policy prevents cross-origin JavaScript from accessing another domain's cookies). However, this pattern can be defeated if the attacker can set cookies on the target domain (via a subdomain or cookie injection vulnerability), so it is considered weaker than the synchronizer token pattern.

The SameSite cookie attribute provides browser-level CSRF protection by controlling when cookies are sent in cross-site requests:

Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly
  • Strict: The cookie is never sent in cross-site requests. If you click a link to the application from an external page, you arrive unauthenticated and must log in again. Maximum security, but impacts usability.
  • Lax: The cookie is sent for top-level navigations (clicking links) but not for cross-site POST requests, image loads, or iframe requests. This blocks CSRF via forms and embedded resources while preserving the logged-in experience for normal link navigation.
  • None: The cookie is sent in all cross-site requests (requires Secure flag). This offers no CSRF protection.

Most modern browsers default to SameSite=Lax when no attribute is specified, which provides baseline protection. However, relying solely on browser defaults is risky — explicit configuration and defense in depth remain essential.

Origin Header Validation

The Origin header is sent with POST requests and indicates the origin that initiated the request. Unlike Referer, it cannot be suppressed by the page that triggers the request. Validating that the Origin matches your application's domain is a strong supplementary defense:

python
def check_origin(request):
    origin = request.headers.get('Origin')
    allowed = {'https://app.example.com'}
    if origin and origin not in allowed:
        abort(403, 'Invalid origin')

This should be used alongside tokens, not as a replacement, because some legitimate requests (such as those from older browsers or certain proxy configurations) may not include the Origin header.

Testing for CSRF

Effective CSRF testing follows a systematic process:

  1. Identify state-changing endpoints — Any request that modifies data, changes settings, creates records, or performs actions on behalf of the user.
  2. Examine the request for anti-CSRF protections — Look for tokens in hidden fields, custom headers, or cookie-to-header correlation.
  3. Attempt to forge the request from a different origin — Create a simple HTML page that submits the same request and verify whether the server accepts it.
  4. Test token validation rigor — Try submitting with no token, an empty token, a token from a different session, or a token from a different form.
  5. Check SameSite cookie settings — Inspect the Set-Cookie headers to determine whether the session cookie has appropriate SameSite restrictions.
  6. Test JSON endpoints — Verify that Content-Type enforcement prevents form-based CSRF against API endpoints.

Key Takeaways

Cross-site request forgery remains dangerous precisely because it requires no vulnerability in the application's code in the traditional sense. There is no injection, no code execution, no memory corruption. The application does exactly what it is designed to do — it processes an authenticated request. The problem is that the user never intended to make that request.

Defense requires deliberate effort:

  1. Include anti-CSRF tokens in every state-changing request and validate them on the server
  2. Set SameSite=Lax or SameSite=Strict on all session cookies
  3. Validate the Origin header as a supplementary check
  4. Never perform state-changing actions via GET requests
  5. Implement step-up authentication (re-enter password, confirm via email) for critical actions like email changes, password resets, and financial transactions

The browser will always send the cookies. Your application must verify that the request behind those cookies was intentional.

Need your application tested for this? Get in touch.

Need your application tested?

We find these vulnerabilities in real applications every day. Get a comprehensive security assessment with detailed remediation.

Request an Assessment

Summary

Cross-site request forgery exploits the browser's automatic inclusion of credentials to trick authenticated users into performing unintended actions. By forging requests that ride on existing sessions, attackers can change passwords, transfer funds, and modify account settings without the user's knowledge.

Key Takeaways

  • 1CSRF exploits the browser's automatic attachment of cookies to force authenticated users into performing unintended state-changing actions
  • 2Attacks require no JavaScript execution on the target site and can be triggered from any web page, email, or embedded resource
  • 3Anti-CSRF tokens tied to user sessions are the primary defense, requiring a secret value that attackers cannot predict or retrieve
  • 4The SameSite cookie attribute provides strong browser-level protection by restricting when cookies are sent in cross-origin requests
  • 5CSRF is most dangerous when combined with predictable request structures and missing secondary confirmation for sensitive actions

Frequently Asked Questions

Cross-site request forgery is an attack that forces an authenticated user to perform actions on a web application without their knowledge. The attacker crafts a malicious request and tricks the victim's browser into sending it, with the browser automatically attaching the victim's session cookies. The server sees a legitimate authenticated request and processes it.

The attacker creates a web page or email containing a forged request to a target application — typically as a hidden form that auto-submits or an image tag with a crafted URL. When the victim visits the attacker's page while logged into the target application, the browser sends the forged request with the victim's session cookie attached. The server cannot distinguish it from a legitimate request.

XSS involves injecting malicious scripts into a trusted website that execute in the victim's browser. CSRF does not require code execution on the target site — it exploits the trust that the server places in the browser's automatic credential handling. XSS attacks the user's trust in a site; CSRF attacks the site's trust in the user's browser.

Anti-CSRF tokens are unique, unpredictable values generated by the server and embedded in forms or request headers. When the server receives a state-changing request, it verifies that the token matches the one it issued. Since the attacker cannot read the token from a cross-origin page due to the same-origin policy, they cannot include a valid token in their forged request.