1. Overview
Cross-site request forgery (CSRF), also known as session riding or one-click attack, takes advantage of the user’s browser’s trust in a web application. When a user is authenticated on a web application, the application assumes that any request made by the user’s browser is deliberate. However, if an attacker deceives the user’s browser into sending a request to the application, the app will treat it as genuine.
In this tutorial, we’ll talk about CSRF, how it works, common attack situations, and how to prevent it.
2. Common CSRF Attack Scenarios
Attackers can utilize sensitive user data, such as email addresses or passwords, to control accounts. They can create a hidden form that sends an email change request when submitted:
The attack succeeds when a targeted user with an active session cookie on the target site (example.com) visits an attacker-controlled page (e.g., via phishing). The page contains a hidden form that JavaScript automatically submits to example.com‘s email endpoint. Additionally, the browser appends the user’s session cookie to the request, enabling the attacker to take control once the server processes the request as legitimate and updates the email.
Finally, by attaching the session cookie, the browser deceives the server into accepting the request as authentic, taking advantage of browsers’ automatic cookie inclusion and servers’ incapacity to detect fraudulent requests.
3. Preventing CSRF Attacks
We must implement multiple measures to prevent CSRF attacks by validating request authenticity. CSRF takes advantage of the browser’s default incorporation of cookies in cross-site requests, unlike Cross-Site Scripting (XSS) or session hijacking, which exploit poor input handling or stolen tokens.
Additionally, while XSS defenses emphasize cleaning user input and session-hijacking mitigations prioritize encryption, CSRF protection focuses on validating requests.
3.1. CSRF Tokens
CSRF tokens verify that the request comes from the user application’s interface. The server generates a unique token for each session and validates it when the user submits a request.
When a request is submitted, the server checks to verify if the request token matches the token kept in the session:
from flask import Flask, request, session, abort
import secrets
app = Flask(__name__)
app.secret_key = secrets.token_hex(16)
@app.route('/update-email', methods=['POST'])
def update_email():
submitted_token = request.form.get('csrf_token')
session_token = session.get('csrf_token')
if not submitted_token or not secrets.compare_digest(submitted_token, session_token):
abort(403)
new_email = request.form.get('new_email')
return "Email updated successfully."
In this example, we receive the csrf_token from the submitted form data. We retrieve the token saved in the user’s session. Then, secrets.compare_digest runs a secure, timing-attack-resistant comparison, and an invalid token results in a 403 response.
CSRF tokens should be generated using secure libraries and associated with the user’s session. For stateless apps, we can use the signed double-submit cookie approach, where the token is HMAC-signed with a server secret and kept in both a cookie and the request parameter.
3.2. Same-Site Cookies
The SameSite attribute for cookies can help avoid CSRF attacks by limiting when cookies are transmitted with cross-site requests. Setting the SameSite property to Lax permits cookies to be delivered only during safe top-level navigation (e.g., clicking a link) but prevents them from being sent in cross-site POST requests.
For instance, let’s say a banking application (baeldungbank.com) creates a session cookie with SameSite=Lax:
response.set_cookie(
'sessionid',
value='user_session_token',
secure=True,
httponly=True,
samesite='Lax'
)
The browser includes the cookie when a user clicks a link to baeldungbank.com from another site:
<a href="https://baeldungbank.com/dashboard">View Dashboard</a>
The browser sends the cookie because the navigation request is a top-level GET. What happens if an attacker embeds a form on a dubious site to initiate a fund transfer?
<form action="https://baeldungbank.com/transfer" method="POST">
<input type="hidden" name="amount" value="1000">
<input type="hidden" name="account" value="attacker-123">
</form>
<script>document.forms[0].submit();</script>
In this example, the browser prevents the cookie from being sent with the cross-site POST request. Thus, the server rejects the unauthenticated operation.
Setting SameSite to Lax strikes a balance between security and usability. Unlike Strict, which prohibits all cross-site cookies, including links, Lax enables safe navigation while preventing state-changing requests like POST.
3.3. Custom Headers
Another solution is to demand special headers (such as X-Requested-With) in AJAX requests. Because browsers enforce the Same-Origin Policy, attackers cannot use custom headers in cross-site requests.
For instance:
fetch('https://example.com/change-email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ new_email: '[email protected]' })
});
In this example, the server uses the X-Requested-With header to confirm that the request originates from a legitimate source.
3.4. User Interactions
Adding additional user involvement, e.g., re-authentication, CAPTCHA, and others, helps prevent sensitive operations from CSRF attacks. For example, it’s common to ask a user to perform an action to prove that they are “not a robot”.
An I’m not a robot checkbox uses behavioral analysis to distinguish between humans and bots. Similarly, image-based CAPTCHAs demand users to recognize objects or patterns that automated scripts fail to understand.
Finally, for crucial activities such as password updates, CSRF tokens can be used with re-authentication or CAPTCHA. This handles login CSRF, which is when attackers hijack unauthenticated sessions.
4. CSRF Prevention Measures
So, we have several tactics to resist CSRF attacks:
Measure
Description
Example
CSRF Tokens
The server validates these unique tokens embedded in forms.
Custom Headers
Include headers like X-Requested-With in AJAX requests.
headers: { ‘X-Requested-With’: ‘XMLHttpRequest’ }
User Re-Authentication
For sensitive actions, credentials are forced to be re-entered.
Password re-entry before changing the account email.
CAPTCHA
Block automated scripts from submitting forged requests.
Google reCAPTCHA or “I’m not a robot” challenges
Framework Protections
Leverage built-in CSRF defenses in web frameworks.
Django’s {% csrf_token %}, Spring Security’s csrf().enable()
Avoid GET for State Changes
POST, PUT, or DELETE for actions modifying data.
Deprecate endpoints like GET /transfer?amount=1000.
Double-Submit Cookies
For stateless apps, a signed token should be stored in a cookie and included as a request parameter.
HMAC-signed token in csrf_cookie and csrf_param (validated server-side).
Integrating various security measures results in a strong defense strategy that provides greater coverage against emerging attack methods.
5. Conclusion
In this article, we talked about CSRF attacks. CSRF is a significant security flaw that can result in unauthorized actions being taken on behalf of users.
CSRF tokens are the most effective defense against CSRF attacks. The SameSite property for cookies can help reduce CSRF vulnerabilities by limiting when cookies are delivered with cross-site requests. Additionally, custom headers and user interaction add extra layers of security.