CSRF (Cross-Site Request Forgery)
Cross-Site Request Forgery (CSRF) is an attack in which an attacker tricks a logged-in user's browser into sending unintended HTTP requests to a web application—on the user's behalf and without their knowledge. It is listed among the OWASP Top 10 and is particularly dangerous in the context of state-changing actions.
CSRF attacks exploit the trust established by browser sessions: Cookies are automatically sent along—even if the request comes from a third-party website. A single click on a malicious link can change account settings, trigger transactions, or take over accounts.
How CSRF Works
The attack scenario for a money transfer involves six steps:
- The victim is logged in to
bank.de(session cookie present). - The attacker creates a malicious webpage at
evil.com/malicious.html. - The victim clicks on a link to
evil.com, e.g., via a phishing email. evil.comloads either a hidden image tag (<img src="https://bank.de/transfer?amount=1000&to=attacker" />) or an automatically submitted hidden form.- The browser sends the request to
bank.de—with the victim’s session cookie—automatically and without the victim’s knowledge. bank.deconsiders the request a legitimate user request and executes the transfer.
Conditions for CSRF
- The victim must be logged in (session cookie valid)
- The application does not verify whether the request originates from its own site
- The attacker knows the target URL (often easy to guess)
- No CSRF protection measures are implemented
CSRF vs. XSS - Key Difference
| Property | XSS | CSRF |
|---|---|---|
| Attack vector | Attacker injects JavaScript into the target website | Attacker tricks the browser into sending a request |
| Control | Attacker controls code within the context of the target site | Attacker does not see the response (Same-Origin Policy) |
| Dangerous Actions | Steal cookies, intercept form data | Only state-changing actions (POST/PUT/DELETE) |
| CSRF Protection | Bypasses CSRF protection (code from the same origin) | GET requests that modify data are particularly vulnerable |
> Mnemonic: XSS allows attackers to read and manipulate browser data. CSRF tricks the browser into performing actions on behalf of the user.
CSRF Protection Measures
1. CSRF Tokens (Synchronizer Token Pattern) - Primary Measure
The server generates a random token, stores it in the session, and embeds it in every form. For every state-changing request, the server checks whether the token sent along matches the session token.
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="a9d3e7b2c4f1..." />
<input name="amount" />
<button>Transfer</button>
</form>```
```typescript
// Express.js with csurf:
import csrf from 'csurf';
const csrfProtection = csrf({ cookie: false }); // Session-based!
app.get('/transfer', csrfProtection, (req, res) => {
res.render('transfer', { csrfToken: req.csrfToken() });
});
app.post('/transfer', csrfProtection, (req, res) => {
// csurf checks the token automatically!
// If token is missing/incorrect → 403 Forbidden
});
2. SameSite Cookie Attribute - Modern Primary Measure
The SameSite attribute controls when cookies are sent along with cross-site requests:
| Value | Behavior | Usage |
|---|---|---|
Strict | Cookie is never sent with cross-site requests - absolute CSRF protection | Problematic: Users arriving via external links are not logged in |
Lax | Cookie only sent with top-level navigation using GET | Recommended for most apps; protects against CSRF forms (POST), allows normal external links |
None; Secure | Cookie sent with all cross-site requests – only with HTTPS | Only for CORS APIs that require cookies (e.g., widgets) |
Set-Cookie: session=abc; SameSite=Lax; Secure; HttpOnly; Path=/
> Note: Since Chrome 80, SameSite=Lax is the default if no SameSite attribute is set.
3. Double Submit Cookie Pattern
As an alternative when server-side sessions are not available:
- Server sets CSRF token as a cookie (not httpOnly!)
- JavaScript reads the cookie and sends the token in the request header
- Server checks: Cookie token == header token?
Advantage: Works without a server-side session. Disadvantage: Vulnerable to subdomain attacks (XSS on subdomain.company.com can read and manipulate the cookie).
4. Custom Request Header (for AJAX/APIs)
APIs that accept only JSON can use the following header checks:
request.headers['X-Requested-With'] == 'XMLHttpRequest'request.headers['Content-Type'] == 'application/json'
Normal browser forms cannot send custom headers—this check therefore functions as implicit CSRF protection. Note: Use only for APIs, not for HTML forms.
Vulnerable HTTP Methods
GET requests should never be state-changing (REST principle). They are particularly vulnerable because browsers, crawlers, and proxies cache and automatically execute GETs. An example of a critical vulnerability: <img src="https://example.com/delete-account">.
POST, PUT, and DELETE requests are the primary targets of CSRF and must be protected with tokens or SameSite.
Exceptions (no CSRF protection required):
- Read-only GET requests without state changes
- Endpoints used exclusively via API (no browser cookie)
- OAuth2 token endpoints (no cookie) — but: implement a
stateparameter as CSRF protection
CSRF in Modern SPAs
Single Page Applications (React, Vue, Angular) send API requests via fetch or axios, not through HTML forms. Since custom headers can be set, there is natural CSRF protection for API calls. However, cookies must still be protected.
Modern Setup for SPAs
Backend (Express) with JWT in the Authorization header:
// JWT in the Authorization header: NO CSRF issue!
// Cross-site forms cannot send an Authorization header.
// Session/Cookie Auth: SameSite=Lax is usually sufficient:
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'lax'
});
Frontend (React) with JWT:
// Authorization Header (JWT): no cookie → no CSRF
fetch('/api/transfer', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ amount: 100 }),
});
// Cookie-based authentication with CSRF token:
fetch('/api/transfer', {
method: 'POST',
credentials: 'include', // Send cookie
headers: {
'X-CSRF-Token': csrfToken, // Token from meta tag or API
'Content-Type': 'application/json',
},
});
Detecting CSRF in Penetration Tests
Manual CSRF Tests
-
Identify state-changing endpoints: Log all POST/PUT/DELETE requests with Burp Suite, remove the CSRF token, and check whether the request is still successful.
-
Check CSRF token quality:
- Is the token random or predictable?
- Is the token valid per session or per request?
- Is the token included in the URL? (URL is logged → token leakage)
- Is the token too short? (less than 128 bits of entropy)
-
Check SameSite cookie:
curl -v https://example.com/loginand check the Set-Cookie header for SameSite. -
Check CORS headers:
Access-Control-Allow-Origin: *combined withAccess-Control-Allow-Credentials: trueis a CORS misconfiguration. -
Referer header check: Send requests without a Referer header and check if they are accepted. This is a weak measure, as the Referer can be forged.
Automated Tools
- Burp Suite: CSRF PoC Generator
- OWASP ZAP: CSRF Scanner
- csrf-poc-generator (npm): automatic PoC generation
Example: CSRF PoC for Reporting
<!-- CSRF Proof-of-Concept (für Pentest-Reports) -->
<!DOCTYPE html>
<html>
<head><title>CSRF PoC</title></head>
<body>
<h1>CSRF Test - Automatic POST to bank.de</h1>
<form id="csrf-form"
action="https://bank.de/api/transfer"
method="POST">
<input type="hidden" name="amount" value="1000" />
<input type="hidden" name="recipient" value="IBAN_ANGREIFER" />
<input type="hidden" name="note" value="CSRF-Test" />
</form>
<script>
// Automatisches Absenden ohne Nutzerinteraktion
document.getElementById('csrf-form').submit();
</script>
</body>
</html>```
### CVSS Rating (Example: CSRF on Bank Transfer)
| Metric | Value |
|---|---|
| Attack Vector | Network |
| Attack Complexity | Low |
| Privileges Required | None |
| User Interaction | Required |
| Scope | Unchanged |
| Confidentiality | None |
| Integrity | High |
| Availability | None |
| **CVSS Base Score** | **6.5 (Medium)** |
The actual severity rating is high if state-changing actions are involved, such as password changes, granting admin privileges, or data deletion.