Schutzstrategien gegen Cross-Site Request Forgery
Umfassender Leitfaden zu CSRF-Angriffen und ihrer Abwehr: tokenbasierter Schutz, SameSite-Cookies, Double-Submit und Framework-Implementierungen.

Cross-Site Request Forgery (CSRF) nutzt das Vertrauen des Browsers in Cookies aus. Wenn ein Nutzer auf deiner Seite authentifiziert ist, sendet sein Browser automatisch Cookies mit jeder Anfrage — auch bei Anfragen, die von einer bösartigen Drittseite ausgelöst werden. CSRF bringt den Browser dazu, authentifizierte Anfragen auszuführen, die der Nutzer nie beabsichtigt hat.
Obwohl CSRF gut verstanden ist, bleiben die Schwachstellen weit verbreitet, weil Entwickler annehmen, moderne Frameworks würden das automatisch abdecken. Manche tun das. Viele nicht, besonders in Architekturen mit SPA plus API.
Wie CSRF funktioniert
Der Angriff erfordert drei Bedingungen: Der Nutzer ist authentifiziert (Cookies vorhanden), die Zielaktion verwendet Cookies zur Authentifizierung, und die Anfrage ist „einfach" genug, dass der Browser sie ohne Preflight-Prüfung sendet.
<!-- ❌ Malicious page hosted on attacker.com -->
<!-- User visits this page while logged into bank.com -->
<!-- Hidden form auto-submits on page load -->
<form id="evil" action="https://bank.com/api/transfer" method="POST">
<input type="hidden" name="to" value="attacker-account" />
<input type="hidden" name="amount" value="10000" />
</form>
<script>
document.getElementById('evil').submit();
</script>
<!-- Browser sends bank.com cookies automatically.
bank.com sees a valid authenticated request
and processes the transfer. -->Der Nutzer hat nie auf einen Überweisungsbutton geklickt. Er hat nur eine Seite besucht. Der Browser hat den Rest erledigt, weil Cookies nicht zwischen Anfragen unterscheiden, die der Nutzer initiiert hat, und solchen, die eine bösartige Seite ausgelöst hat.
Synchronizer-Token-Pattern
Die häufigste CSRF-Abwehr: serverseitig ein zufälliges Token erzeugen, in die Seite einbetten und bei jeder zustandsändernden Anfrage validieren.
import crypto from 'crypto';
// Generate a CSRF token and store it in the session
function generateCsrfToken(session: Session): string {
const token = crypto.randomBytes(32).toString('hex');
session.csrfToken = token;
return token;
}
// Middleware: validate token on state-changing requests
function csrfProtection(req: Request, res: Response, next: NextFunction) {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next(); // Safe methods don't need CSRF protection
}
const sessionToken = req.session?.csrfToken;
const requestToken =
req.headers['x-csrf-token'] ??
req.body?._csrf;
if (!sessionToken || !requestToken) {
return res.status(403).json({ error: 'CSRF token missing' });
}
// Constant-time comparison prevents timing attacks
const isValid = crypto.timingSafeEqual(
Buffer.from(sessionToken),
Buffer.from(requestToken as string)
);
if (!isValid) {
return res.status(403).json({ error: 'CSRF token invalid' });
}
next();
}<!-- Embed the token in forms -->
<form action="/api/transfer" method="POST">
<input type="hidden" name="_csrf" value="{{csrfToken}}" />
<input type="text" name="to" placeholder="Recipient" />
<input type="number" name="amount" placeholder="Amount" />
<button type="submit">Transfer</button>
</form>Der Angreifer kann das CSRF-Token nicht lesen, weil die Same-Origin-Policy das Lesen fremder Seiten verhindert. Er kann ein Formular an deine Domain abschicken, aber nicht das Token beifügen, das er nicht besitzt.
Double-Submit-Cookie-Pattern
Für zustandslose APIs ohne serverseitige Sessions nutzt das Double-Submit-Pattern ein Cookie-Header-Paar statt eines in der Session gespeicherten Tokens.
// ❌ Stateless API with no CSRF protection
app.post('/api/settings', authenticate, (req, res) => {
// Authenticates via cookie — vulnerable to CSRF
updateSettings(req.user.id, req.body);
res.json({ success: true });
});// ✅ Double submit cookie pattern
import crypto from 'crypto';
// On login: set a CSRF cookie (not HttpOnly — JS must read it)
function setCsrfCookie(res: Response): void {
const token = crypto.randomBytes(32).toString('hex');
res.cookie('csrf-token', token, {
sameSite: 'strict',
secure: true,
httpOnly: false, // JavaScript must read this cookie
path: '/',
});
}
// Middleware: compare cookie value with header value
function doubleSubmitCsrf(req: Request, res: Response, next: NextFunction) {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next();
}
const cookieToken = req.cookies['csrf-token'];
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || !headerToken) {
return res.status(403).json({ error: 'CSRF token missing' });
}
const isValid = crypto.timingSafeEqual(
Buffer.from(cookieToken),
Buffer.from(headerToken as string)
);
if (!isValid) {
return res.status(403).json({ error: 'CSRF token mismatch' });
}
next();
}
// Client-side: read cookie and send as header
async function apiRequest(url: string, data: unknown) {
const csrfToken = document.cookie
.split('; ')
.find(row => row.startsWith('csrf-token='))
?.split('=')[1];
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken ?? '',
},
credentials: 'include',
body: JSON.stringify(data),
});
}Der Angreifer kann eine Anfrage auslösen, die das Cookie mitsendet, aber er kann den Cookie-Wert nicht lesen (Same-Origin-Policy) und daher nicht den passenden Header setzen. Der Server lehnt Anfragen ab, bei denen Cookie und Header nicht übereinstimmen.
Das SameSite-Cookie-Attribut
Moderne Browser unterstützen das Cookie-Attribut SameSite, das verhindert, dass der Browser Cookies mit Cross-Origin-Anfragen sendet.
// ❌ Cookie without SameSite — sent on all requests including cross-origin
res.cookie('session', sessionId, {
httpOnly: true,
secure: true,
});
// ✅ SameSite=Strict — cookie only sent on same-origin navigation
res.cookie('session', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'strict',
});
// ✅ SameSite=Lax — sent on same-origin + top-level GET navigations
res.cookie('session', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'lax', // Good default — allows "Login with Google" flows
});SameSite=Lax ist der Standard in modernen Browsern und verhindert CSRF bei POST-Anfragen. Strict bietet stärkeren Schutz, bricht aber legitime siteübergreifende Navigationsflüsse (ein Klick auf einen Link zu deiner Seite aus einer E-Mail heraus sendet das Session-Cookie nicht mit).
SameSite ist Defense-in-Depth. Verlasse dich nicht allein darauf — ältere Browser unterstützen es nicht, und Lax erlaubt weiterhin GET-basiertes CSRF bei zustandsändernden GET-Endpunkten (die es nicht geben sollte, aber manchmal gibt).
Pflicht für einen eigenen Header
Für APIs, die nur von JavaScript konsumiert werden (nicht von Formular-Submits), ist das Erzwingen eines eigenen Headers, den Browser nicht automatisch senden, eine einfache Abwehr.
// Middleware: require a custom header on all requests
function requireCustomHeader(req: Request, res: Response, next: NextFunction) {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next();
}
// Browsers don't add X-Requested-With to form submissions
// Only JavaScript can set custom headers (triggers CORS preflight)
if (req.headers['x-requested-with'] !== 'XMLHttpRequest') {
return res.status(403).json({ error: 'Missing required header' });
}
next();
}
// Client-side: add the header to all requests
const api = {
post: (url: string, data: unknown) =>
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
credentials: 'include',
body: JSON.stringify(data),
}),
};Das funktioniert, weil ein Cross-Origin-Formular-Submit keine eigenen Header setzen kann. Allein das Setzen von Content-Type: application/json löst einen CORS-Preflight aus, der die Anfrage blockiert, sofern der Server den Origin nicht ausdrücklich erlaubt. Diese Abwehr setzt allerdings eine saubere CORS-Konfiguration voraus — wenn du mit Access-Control-Allow-Origin: * alle Origins erlaubst, wird dieser Schutz aufgeweicht.
Defense in Depth
Keine einzelne CSRF-Abwehr reicht allein aus. Kombiniere mehrere Strategien in Schichten.
// Production CSRF configuration — multiple layers
const csrfConfig = {
// Layer 1: SameSite cookies (browser-level)
cookieOptions: {
sameSite: 'lax' as const,
secure: true,
httpOnly: true,
},
// Layer 2: CSRF token validation (application-level)
tokenValidation: true,
// Layer 3: Origin/Referer header check
originCheck: true,
// Layer 4: Custom header requirement for API routes
customHeaderRequired: true,
};
function fullCsrfProtection(req: Request, res: Response, next: NextFunction) {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
return next();
}
// Check Origin header
const origin = req.headers.origin ?? req.headers.referer;
if (origin) {
const allowedOrigins = [process.env.APP_URL];
const requestOrigin = new URL(origin).origin;
if (!allowedOrigins.includes(requestOrigin)) {
return res.status(403).json({ error: 'Origin not allowed' });
}
}
// Validate CSRF token (synchronizer or double-submit)
// ... token validation logic ...
next();
}Die wichtigsten Erkenntnisse
- CSRF nutzt die automatische Cookie-Mitlieferung aus — Browser senden Cookies standardmäßig auch mit Cross-Origin-Anfragen
- Nutze SameSite=Lax-Cookies als Basisabsicherung — verhindert CSRF bei POST, erlaubt aber normale Navigation
- Implementiere tokenbasierten Schutz für sessionbasierte Apps — Synchronizer-Tokens oder Double-Submit-Cookies
- Erzwinge eigene Header auf API-Endpunkten — Browser fügen Formular-Submits keine eigenen Header hinzu
- Verwende zeitkonsistente Vergleiche bei der Token-Validierung —
crypto.timingSafeEqualverhindert Timing-Angriffe - Kombiniere mehrere Abwehrschichten — SameSite-Cookies + Token-Validierung + Origin-Prüfung ergeben zusammen einen robusten Schutz


