Situation actuelle : PHP2Shell. Deux failles PHP critiques (CVSS 9.2) permettent d'exécuter du code malveillant sans authentification. Correctifs disponibles depuis le 21 octobre 2026.Ce qu'il faut faire maintenant →

Protection CSRF dans les applications PHP

Comment les développeurs empêchent le Cross-Site Request Forgery dans les applications PHP : des jetons pour chaque requête qui modifie quelque chose, un contrôle de l'origine dans l'en-tête et des cookies SameSite comme couche supplémentaire.

État 7 min de lectureDéveloppement sécurisé

En bref : vous empêchez le CSRF en PHP en exigeant que chaque requête qui modifie quelque chose apporte un jeton secret lié à la session, que vous vérifiez côté serveur avec hash_equals(). Vous contrôlez en plus les en-têtes Origin ou Sec-Fetch-Site et définissez les cookies de session avec SameSite. Les requêtes GET ne doivent jamais modifier de données, et le formulaire de connexion a lui aussi besoin de protection.

Un clic que personne ne remarque

Une collaboratrice est connectée au back-office de la boutique et ouvre un site tiers dans un second onglet. Ce site envoie en arrière-plan un formulaire au back-office, par exemple pour changer une adresse e-mail ou créer un compte. Le navigateur joint automatiquement le cookie de session. Pour l'application, la requête ressemble à n'importe quelle autre venant de sa propre équipe.

C'est le Cross-Site Request Forgery, en abrégé CSRF : une requête qu'un utilisateur connecté envoie à son insu à une application en laquelle il a confiance. L'attaquant ne voit pas la réponse, mais n'en a pas besoin. Il suffit que l'action soit exécutée.

Le CSRF ne fait pas la une comme PHP2Shell, mais c'est un constat fréquent dans les applications PHP développées en interne. Les frameworks comme Symfony et Laravel intègrent une protection ; les failles y apparaissent surtout là où elle a été désactivée pour certaines routes. Les développements maison sans framework n'en ont souvent aucune.

CSRF en PHP : quelles requêtes doivent être protégées

Toutes les requêtes qui modifient un état ont besoin de protection : enregistrer, supprimer, commander, attribuer des droits, modifier des paramètres, téléverser des fichiers, se déconnecter. Les consultations en lecture n'ont pas besoin de jeton, tant qu'elles ne font réellement que lire.

D'où la première règle : aucune modification par GET. Des liens comme loeschen.php?id=… sont particulièrement faciles à déclencher depuis des sites tiers, par exemple via une image intégrée. Les actions qui modifient relèvent de POST, PUT, PATCH ou DELETE.

La seconde règle : protéger aussi la connexion. Avec ce qu'on appelle le CSRF de connexion, une victime est connectée à son insu à un compte de l'attaquant. Si elle y saisit des données de paiement ou des documents, ceux-ci aboutissent chez l'attaquant. Un jeton dans le formulaire de connexion et un nouvel identifiant de session après une authentification réussie l'empêchent.

Le modèle du jeton synchronisé en PHP

Pour les applications classiques avec sessions, l'OWASP recommande le modèle du jeton synchronisé : le serveur génère un jeton aléatoire, le stocke dans la session et l'attend en retour dans chaque formulaire qui modifie quelque chose. L'important est que le jeton soit transmis dans le formulaire ou dans un en-tête dédié, et non dans un cookie.

<?php
declare(strict_types=1);

function csrfToken(): string
{
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

function csrfPruefen(): void
{
    $gesendet = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
    if (!is_string($gesendet) || !hash_equals(csrfToken(), $gesendet)) {
        http_response_code(403);
        exit('Anfrage abgelehnt.');
    }
}

if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
    csrfPruefen();
}
?>
<form method="post" action="/konto/email">
    <input type="hidden" name="csrf_token"
           value="<?= htmlspecialchars(csrfToken(), ENT_QUOTES, 'UTF-8') ?>">
    <!-- autres champs -->
</form>

hash_equals() compare en temps constant, de sorte que le temps de réponse ne révèle rien sur la bonne valeur. Après la connexion, vous devriez renouveler l'identifiant de session avec session_regenerate_id(true) et générer un nouveau jeton. Pourquoi random_bytes() est la bonne source et quelles fonctions sont inadaptées est expliqué dans Nombres aléatoires et jetons sûrs en PHP.

Un jeton par session suffit pour la plupart des applications. Des jetons par formulaire ou par requête renforcent un peu la protection, mais posent des problèmes avec plusieurs onglets ouverts et le bouton Retour. Plus important que la fréquence de renouvellement : le jeton ne doit jamais apparaître dans des URL, où il peut finir dans les journaux, dans l'historique du navigateur et, via le Referer, chez des sites tiers.

Pour les applications sans état et sans session, l'OWASP cite comme alternative le cookie à double soumission signé : un jeton lié à la session par HMAC. La variante simple, non signée, n'est expressément pas recommandée.

Deuxième couche : Origin, Sec-Fetch-Site et SameSite

Les navigateurs modernes envoient avec les requêtes des informations sur leur origine. L'en-tête Origin indique la page appelante ; Sec-Fetch-Site indique si la requête provient de la même origine (same-origin), d'un site apparenté (same-site) ou d'un site tiers (cross-site). On peut en tirer un contrôle complémentaire simple :

<?php
$erlaubteHerkunft = 'https://www.example.de';
$fetchSite = $_SERVER['HTTP_SEC_FETCH_SITE'] ?? null;
$origin    = $_SERVER['HTTP_ORIGIN'] ?? null;

$verdaechtig = ($fetchSite !== null && $fetchSite !== 'same-origin')
    || ($fetchSite === null && $origin !== null && $origin !== $erlaubteHerkunft);

if ($_SERVER['REQUEST_METHOD'] !== 'GET' && $verdaechtig) {
    http_response_code(403);
    exit;
}

L'OWASP souligne deux points : l'origine doit être comparée intégralement afin que des domaines à consonance voisine ne passent pas, et un en-tête absent ou la valeur null ne doit pas être considéré comme l'origine du site. Comme les navigateurs plus anciens n'envoient pas toujours ces en-têtes, le jeton reste la défense principale.

La troisième couche, ce sont les cookies de session avec SameSite. PHP prend en charge session.cookie_samesite depuis la version 7.3 ; la valeur par défaut est vide. Définissez au minimum Lax ; pour les back-offices purs, Strict est souvent possible. Les autres réglages des cookies sont décrits dans le guide Sécuriser les sessions en PHP.

Si vous utilisez un CMS, il apporte en général ses propres outils. WordPress, par exemple, fonctionne avec ce qu'on appelle des nonces, que les extensions intègrent via des fonctions comme wp_nonce_field() et check_admin_referer(). En pratique, les failles apparaissent surtout dans des extensions qui oublient ces contrôles pour certaines actions. Vérifiez donc tout particulièrement les extensions que vous avez écrites vous-même ou qui sont peu répandues.

Pour les interfaces utilisées par des navigateurs depuis d'autres domaines, la configuration CORS est en outre déterminante. Si le serveur renvoie n'importe quelle origine demandeuse comme autorisée tout en admettant les cookies, il neutralise lui-même la protection du navigateur. L'OWASP recommande de n'ouvrir CORS qu'aux origines explicitement de confiance ; une autorisation générale de tous les sous-domaines devient un problème dès que l'un d'eux tombe en de mauvaises mains.

Liste de contrôle pour les applications existantes

Pour les projets qui se sont étoffés au fil du temps, un examen systématique aide :

  1. Recenser toutes les routes et tous les scripts qui modifient des données, y compris les points d'accès Ajax et les téléversements de fichiers.
  2. Vérifier si GET est utilisé pour cela, et passer à POST ou à des méthodes appropriées.
  3. Pour chaque route qui modifie quelque chose, prouver qu'un jeton est vérifié ; rechercher ciblement les exceptions dans le framework.
  4. Tester séparément la connexion, la déconnexion et le changement de mot de passe.
  5. Ajouter session.cookie_samesite et le contrôle de l'origine.
  6. Corriger les failles XSS, car du code injecté peut lire n'importe quel jeton. La démarche est décrite dans Empêcher le cross-site scripting en PHP.
Important

Une protection CSRF vérifiée uniquement dans le front-end par JavaScript est sans effet. Le contrôle doit avoir lieu sur le serveur, avant l'exécution de l'action.

Comment PHP SOS aide-t-il concrètement ?

  • Nous recensons toutes les routes de votre application PHP qui modifient quelque chose et vérifions où un jeton manque ou a été désactivé.
  • Nous mettons en place un point de contrôle central, au lieu de répartir les vérifications de jeton dans de nombreux scripts.
  • Nous ajoutons le contrôle de l'origine et les réglages SameSite, sans perturber les parcours de paiement ou de connexion.
  • Après la migration, nous testons à nouveau et documentons les résultats.

Une vérification complète de votre application est décrite dans la prestation audit de sécurité et test d'intrusion.

Questions fréquentes

SameSite=Lax suffit-il comme protection CSRF ?

Non. Selon l'OWASP, les cookies SameSite sont une couche supplémentaire, pas un remplacement. Lax envoie toujours les cookies lors des consultations normales de pages par GET, et les attaques depuis des sous-domaines du même site ne sont pas couvertes.

Les API JSON ont-elles besoin d'un jeton CSRF ?

Si l'API s'authentifie avec des cookies, oui, ou d'un contrôle équivalent. Une alternative consiste en des en-têtes de requête personnalisés, que les navigateurs n'autorisent depuis des sites tiers qu'après une vérification préalable CORS. L'autorisation CORS ne doit alors inclure que des origines explicitement de confiance.

Le formulaire de connexion doit-il aussi être protégé ?

Oui. Avec le CSRF de connexion, une victime est connectée à son insu à un compte étranger et y saisit ses propres données. Un jeton dans le formulaire de connexion et une nouvelle session après l'authentification l'empêchent.

La protection CSRF protège-t-elle aussi contre XSS ?

C'est l'inverse : une faille XSS neutralise toute protection CSRF, car du code injecté peut, au nom de la page, lire des jetons valides et les renvoyer. Les deux vulnérabilités doivent être corrigées séparément.

Sources

Contact immédiat

Chaque heure compte. Écrivez-nous maintenant.

Décrivez brièvement votre système, votre version de PHP et ce que vous avez observé. Nous vous répondons, faisons le point et travaillons à distance jusqu'à la clôture et la documentation de l'incident.

Aide immédiate