Chapitre 9.5 — Doctrine, secrets & audit
⏱️ TL;DR — Dernier tour de la sécurité Symfony. Doctrine paramètre par défaut (pas de SQLi tant qu’on passe par
setParameteret non par concaténation en DQL/SQL natif — rappel Partie 6). Les formulaires Symfony intègrent une protection CSRF (jeton automatique) ; les templates Twig auto-échappent (anti-XSS — Partie 7),|rawétant à proscrire sur de l’entrée. On pose les en-têtes de sécurité (via le reverse proxy ou un bundle type Nelmio). Les secrets (clés d’API, DB) vont dans le secrets vault de Symfony ou des variables d’environnement — jamais commités. Et on audite les dépendances aveccomposer audit(+symfony check:security), intégré à la CI.
🎯 Objectifs
- Utiliser Doctrine sans injection (paramétrage, ne pas concaténer en DQL/SQL natif).
- S’appuyer sur la protection CSRF des formulaires et l’auto-échappement Twig.
- Poser les en-têtes de sécurité en Symfony (bundle / reverse proxy).
- Gérer les secrets (vault Symfony,
.env.local) sans les commiter. - Auditer les dépendances (
composer audit) et l’intégrer à la CI.
Doctrine : paramétré, si tu ne le casses pas
Rappel de la Partie 6 : Doctrine (ORM et DBAL) utilise des paramètres liés par défaut. Tant que les entrées passent par setParameter (DQL/QueryBuilder) ou les paramètres de executeQuery (DBAL), il n’y a pas d’injection :
// ✅ Parametre : sur
$qb->where('c.titre = :t')->setParameter('t', $titre);
// ❌ Concatenation en DQL : on a CASSE la protection
$dql = "SELECT c FROM App\Entity\Cours c WHERE c.titre = '".$titre."'"; // VULNERABLE
// ❌ SQL natif concatene
$conn->executeQuery("SELECT * FROM cours WHERE titre = '$titre'"); // VULNERABLELa règle d’équipe : jamais de concaténation d’entrée dans une requête, DQL ou SQL natif. Pour un tri/colonne dynamique (non paramétrable), allow-list (Partie 6). En revue, on cherche les "... " . $x et les template strings dans les requêtes.
Formulaires : CSRF intégré, et Twig échappe
CSRF — Les formulaires Symfony (composant Form) incluent automatiquement un jeton CSRF caché, vérifié à la soumission. Tu n’as rien à faire pour un formulaire standard ; pour une action hors formulaire (lien, appel AJAX), tu génères/valides un jeton via csrf_token() / isCsrfTokenValid().
{# Le jeton CSRF est injecte automatiquement par form_widget/form_rest #}
{{ form_start(form) }}
{{ form_row(form.titre) }}
<button>Enregistrer</button>
{{ form_end(form) }} {# inclut le champ _token #}// Action manuelle : verifier le jeton
if (!$this->isCsrfTokenValid('supprimer_cours'.$cours->getId(), $request->request->get('_token'))) {
throw $this->createAccessDeniedException();
}XSS — Twig auto-échappe les variables par défaut ({{ x }} → contexte HTML échappé), ce qui neutralise le XSS de base (Partie 7). Le danger revient avec le filtre |raw (qui désactive l’échappement) : à proscrire sur toute donnée utilisateur. Pour du HTML riche légitime, on sanitize (DOMPurify côté rendu, ou le composant HtmlSanitizer de Symfony) avant d’afficher.
⚠️ Piège —
{{ contenuUtilisateur|raw }}dans un template Twig est un stored XSS direct sicontenuUtilisateurvient d’un utilisateur :|rawdésactive l’auto-échappement. Ne l’utilise jamais sur de l’entrée. Pour afficher du HTML riche autorisé, passe par le HtmlSanitizer de Symfony (ou DOMPurify) qui applique une allow-list de balises sûres, puis rends le résultat. Cherche tous les|rawen revue.
En-têtes de sécurité en Symfony
Les en-têtes de sécurité (CSP, HSTS, nosniff, frame-ancestors… — Partie 2) se posent soit au niveau du reverse proxy (Nginx — Serveur Linux), soit dans l’appli. Côté Symfony, on peut les ajouter via un listener de réponse ou un bundle dédié (par ex. NelmioSecurityBundle, qui gère CSP avec nonces, frame-options, etc.).
// Exemple : ajouter des en-tetes via un listener sur kernel.response
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
// CSP : de preference via un bundle qui gere les nonces (voir Partie 7)L’important : que les en-têtes soient posés partout, de façon cohérente, et une CSP stricte (nonces, pas d’unsafe-inline) déployée d’abord en Report-Only.
Secrets : vault et .env, jamais dans Git
Les secrets (mot de passe de base, clés d’API, clé de signature) ne doivent jamais être commités (Partie 13). Symfony offre deux mécanismes :
- Variables d’environnement via
.env: le fichier.env(valeurs par défaut, non secrètes) est commité, mais.env.local(surcharges secrètes locales) est dans le.gitignore. En prod, on injecte les vraies valeurs par de vraies variables d’environnement (pas un fichier commité). - Secrets vault de Symfony :
php bin/console secrets:setchiffre les secrets ; la clé de déchiffrement de prod (prod.decrypt.private.php) n’est pas commitée et est fournie à l’environnement de prod. Cela permet de versionner les secrets chiffrés en sécurité.
# Vault Symfony : definir un secret (chiffre)
php bin/console secrets:set DATABASE_PASSWORD
# la cle privee de dechiffrement prod n'est PAS commitee💡 Réflexe — Deux règles secrets en Symfony : (1) rien de sensible dans
.env(commité) — les vraies valeurs vont dans.env.local(ignoré) en local et dans de vraies variables d’env / le vault en prod ; (2) la clé de déchiffrement du vault de prod ne se commite jamais. Si un secret a fuité dans l’historique Git, il faut le révoquer et le régénérer (Partie 13), pas juste le supprimer du dernier commit.
Auditer les dépendances
Symfony et Composer facilitent le suivi des vulnérabilités connues dans les dépendances (Partie 12) :
composer audit # signale les paquets installes avec des vulnerabilites connues
symfony check:security # (CLI Symfony) equivalent, base sur l'advisory databaseOn intègre composer audit à la CI : un build échoue si une dépendance a une vulnérabilité connue, forçant la mise à jour. On combine avec un outil de mise à jour automatisée (Dependabot/Renovate — Partie 12) pour ne pas laisser traîner les correctifs.
🎯 Côté attaquant — Contre une appli Symfony correctement configurée, l’injection et le XSS de base sont fermés (Doctrine, Twig). L’attaquant se rabat sur : les
|rawoubliés (XSS), les requêtes concaténées hors des rails Doctrine (SQLi), les dépendances non à jour (une CVE dans un bundle), les secrets fuités dans l’historique Git ou dans un.envcommité, et — toujours — les oublis d’autorisation par objet (voters, chapitre 9.4). Les frameworks élèvent le niveau ; les failles viennent des contournements et de l’hygiène (deps, secrets).
🧭 Sur FormaCampus — Le back Symfony de FormaCampus coche l’ensemble : Doctrine paramétré partout (aucune concaténation, tri par allow-list), CSRF intégré aux formulaires + jeton pour la SPA, Twig auto-échappé (
|rawinterdit, HtmlSanitizer pour le contenu riche des cours), en-têtes de sécurité posés au proxy + CSP à nonces, secrets dans le vault Symfony (clé de prod hors Git) avec.env.localignoré, etcomposer auditen CI (build cassé si CVE) doublé de Dependabot. Un secret ayant fuité par le passé a été révoqué et régénéré, pas seulement retiré du dernier commit. C’est l’application concrète, sur ce stack, de tout ce qu’on a vu depuis la Partie 1.
✏️ Exercices
Exercice 1 — Repère les deux failles. Ce contrôleur affiche et filtre des cours. Trouve les deux problèmes.
$titre = $request->query->get('q');
$dql = "SELECT c FROM App\Entity\Cours c WHERE c.titre LIKE '%".$titre."%'";
$cours = $em->createQuery($dql)->getResult();
// vue Twig : {{ cours[0].description|raw }}✅ Solution
(1) Injection SQL : $titre (entrée) est concaténé dans le DQL → on a contourné le paramétrage de Doctrine. Correctif : WHERE c.titre LIKE :q avec ->setParameter('q', '%'.$titre.'%') (et échapper les jokers %/_ si l’utilisateur ne doit pas les piloter). (2) XSS stocké : {{ cours[0].description|raw }} — le filtre |raw désactive l’auto-échappement de Twig sur une donnée (la description, potentiellement saisie par un utilisateur) → un <script>/<img onerror> s’exécute. Correctif : retirer |raw (afficher échappé), ou, si du HTML riche est légitime, sanitizer via le HtmlSanitizer de Symfony avant d’afficher. Deux motifs classiques : concaténation en DQL et |raw sur de l’entrée.
Exercice 2 — Secret commité. Un .env versionné contient DATABASE_PASSWORD=Sup3rSecret. Quelqu’un propose de « juste le retirer dans le prochain commit ». Est-ce suffisant ?
✅ Solution
Non, insuffisant. Le secret reste dans l’historique Git : quiconque a accès au dépôt (ou à un clone/fork/backup) peut le retrouver dans les commits passés. Le retirer du dernier commit ne l’efface pas de l’historique. La bonne réponse : (1) considérer le secret comme compromis et le révoquer/régénérer immédiatement (changer le mot de passe de la base) ; (2) déplacer la valeur hors de .env (commité) vers .env.local (ignoré) en local, une vraie variable d’environnement ou le vault Symfony en prod ; (3) ajouter .env.local au .gitignore (par défaut dans Symfony) et, si besoin, purger l’historique (avec prudence). La règle : un secret qui a touché Git est grillé — on le change, on ne se contente pas de le cacher.
🧠 Quiz de révision
1. Doctrine protège-t-il de la SQLi, et comment le casse-t-on ?
Oui si on paramètre (setParameter, paramètres de executeQuery). On casse la protection en concaténant une entrée dans le DQL ou dans une requête SQL native. Règle : jamais de concaténation ; allow-list pour un tri/colonne dynamique.
2. Comment Symfony protège-t-il les formulaires du CSRF ?
Le composant Form ajoute automatiquement un jeton CSRF caché (_token), vérifié à la soumission. Pour une action hors formulaire, on utilise csrf_token()/isCsrfTokenValid() manuellement.
3. Quel filtre Twig rouvre le XSS et que faire à la place ?
|raw, qui désactive l’auto-échappement — à proscrire sur toute donnée utilisateur (stored XSS). À la place : laisser l’échappement par défaut (afficher du texte), ou sanitizer le HTML riche (HtmlSanitizer de Symfony / DOMPurify) avant affichage.
4. Où mettre les secrets en Symfony, et où surtout pas ?
Dans le secrets vault (secrets chiffrés versionnés, clé de déchiffrement de prod hors Git) ou des variables d’environnement (.env.local ignoré en local, vraies variables en prod). Jamais dans .env commité ni dans le code. Un secret fuité dans Git doit être révoqué/régénéré.
5. Comment surveille-t-on les dépendances vulnérables ?
Avec composer audit (et symfony check:security), qui signalent les paquets aux vulnérabilités connues. On l’intègre à la CI (build cassé si CVE) et on le double d’un outil de mise à jour (Dependabot/Renovate) pour appliquer les correctifs rapidement (Partie 12).
Fin de la Partie 9. Tu sais sécuriser un back PHP/Symfony de bout en bout : pièges du langage, uploads/LFI/désérialisation, firewalls & auth, voters (anti-IDOR), Doctrine, CSRF, en-têtes, secrets et audit. On passe au versant JavaScript moderne : la Partie 10 — Sécurité Next.js / React / Node.