Skip to Content
SécuritéPartie 9 — Sécurité PHP & Symfony9.3 — Symfony : firewalls & auth

Chapitre 9.3 — Symfony : firewalls & auth

⏱️ TL;DR — Symfony fournit un composant Security mûr et secure by default — à condition de le comprendre. Le firewall intercepte les requêtes d’une zone (main, admin) et gère l’authentification via des authenticators (form login, JSON login, access token, remember me…). Le security.yaml est le cœur : il déclare les firewalls, le password hasher (auto = argon2id/bcrypt — rappel Partie 3), les providers d’utilisateurs, et l’access control. Les mots de passe passent par UserPasswordHasherInterface (jamais en clair), la session est gérée par le framework (rotation à la connexion incluse). Le piège : désactiver ces protections « pour que ça marche » (route hors firewall, hasher plaintext, security: false).

🎯 Objectifs

  • Comprendre le rôle du firewall et sa configuration dans security.yaml.
  • Configurer le password hasher (auto) et hacher/vérifier via UserPasswordHasherInterface.
  • Comprendre les authenticators (form login, etc.) et le cycle d’authentification.
  • Éviter les pièges : routes hors firewall, hasher faible, access_control mal ordonné.

Le firewall : la porte d’entrée

Dans Symfony, la sécurité d’accès est organisée autour des firewalls. Un firewall couvre un ensemble d’URL (défini par un pattern) et décide comment les requêtes de cette zone sont authentifiées. La plupart des applis ont un firewall principal (main) couvrant tout le site, parfois un firewall séparé pour une zone d’API ou d’admin.

# config/packages/security.yaml (structure simplifiee) security: password_hashers: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' providers: app_users: entity: { class: App\Entity\User, property: email } firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false # ressources de dev, hors securite main: lazy: true provider: app_users form_login: login_path: app_login check_path: app_login enable_csrf: true # protection CSRF du formulaire de login logout: path: app_logout remember_me: secret: '%kernel.secret%' # ... access_control: - { path: ^/admin, roles: ROLE_ADMIN } - { path: ^/profil, roles: ROLE_USER }

Points de sécurité de cette config :

  • password_hashers: 'auto' : Symfony choisit le meilleur algorithme disponible (argon2id/bcrypt) et gère le sel/coût. On ne configure jamais plaintext ou un hachage faible.
  • enable_csrf: true sur form_login : protège le formulaire de connexion contre le CSRF.
  • access_control : les règles d’accès par URL, évaluées dans l’ordre (la première qui matche gagne — piège fréquent, voir plus bas).

Le password hasher : ne jamais toucher au clair

Le hachage des mots de passe (Partie 3) est géré par le composant. À l’inscription/au changement, on passe le mot de passe en clair au hasher, qui renvoie le hash à stocker ; à la connexion, le firewall vérifie automatiquement.

// A l'inscription : hacher via le service (jamais stocker le clair) public function register(UserPasswordHasherInterface $hasher, Request $request): Response { $user = new User(); $user->setEmail($email); $user->setPassword($hasher->hashPassword($user, $plainPassword)); // argon2id via 'auto' // ... persister }

Le mot de passe en clair ($plainPassword) ne doit pas être persisté ni loggué. Symfony a d’ailleurs un attribut/#[Ignore] et des conventions pour éviter de sérialiser le clair. La vérification à la connexion est faite par le firewall (comparaison en temps constant incluse) — tu n’écris pas la comparaison toi-même.

⚠️ Piège — Ne configure jamais un hasher plaintext ou un algorithme faible « pour tester » (ça se retrouve en prod), et ne compare jamais les mots de passe à la main. Laisse auto : Symfony gère l’algorithme fort, le sel, le coût, la comparaison en temps constant et le rehash automatique quand le coût évolue. Toute tentative de « faire plus simple » ici casse la sécurité.

Les authenticators : comment on prouve son identité

Symfony (composant Security moderne) gère l’authentification via des authenticators. Les plus courants, activés dans security.yaml :

  • form_login : connexion par formulaire (email/mot de passe), avec enable_csrf.
  • json_login : connexion par payload JSON (pour une SPA/API sur le même domaine).
  • access_token : authentification par jeton (API), via un handler qui valide le token.
  • remember_me : cookie « se souvenir de moi » (signé) — à configurer avec soin (durée, secret).
  • Authenticator personnalisé : en implémentant l’interface dédiée pour des cas spécifiques (SSO, magic link…).

Le firewall orchestre le tout : il déclenche l’authenticator, gère l’échec (retour au login), le succès (création de la session, régénération de l’id de session — anti-fixation, Partie 4), et expose l’utilisateur courant ($this->getUser(), #[CurrentUser]).

💡 Réflexe — Utilise les authenticators fournis plutôt que de réinventer l’authentification. Ils intègrent déjà les bonnes pratiques (CSRF sur le login, rotation de session, gestion des échecs, protection contre certaines attaques). Pour un besoin spécifique (SSO OIDC — Partie 4, magic link), écris un authenticator personnalisé en t’appuyant sur le composant, plutôt que du code d’auth « à la main » hors framework.

L’access control et son piège d’ordre

access_control protège des URL par rôle/attribut, globalement. Deux points de vigilance :

  1. L’ordre compte : Symfony applique la première règle dont le path matche, puis s’arrête. Une règle trop large placée avant une plus spécifique peut « avaler » cette dernière.
# ❌ Mauvais ordre : /admin/... matche la 1re regle (ROLE_USER) et s'arrete access_control: - { path: ^/admin/public, roles: PUBLIC_ACCESS } - { path: ^/admin, roles: ROLE_ADMIN } # jamais atteinte pour /admin/public... et OK pour le reste # Regle : du plus SPECIFIQUE au plus general.
  1. access_control ne suffit pas : il protège des zones d’URL (contrôle grossier), mais pas l’accès par objet (l’IDOR de la Partie 5). Un ROLE_USER autorisé sur /facture peut toujours demander la facture d’un autre si le contrôleur ne vérifie pas l’appartenance. C’est le rôle des voters (chapitre 9.4).

🎯 Côté attaquant — Sur une appli Symfony, l’attaquant cherche : des routes hors firewall (ex. une zone d’API oubliée, security: false mal placé), un access_control mal ordonné laissant passer une zone censée être protégée, un remember_me faible, ou — le plus fréquent — un contrôle par URL présent mais aucun contrôle par objet (IDOR). Il teste les endpoints d’admin sans le rôle, et manipule les ids d’objets. Symfony est solide si la config est stricte ; les failles viennent des contournements et des oublis d’autorisation par objet.

🧭 Sur FormaCampus — Le back Symfony de FormaCampus a un firewall main (site + SPA via json_login sur le même domaine) et un firewall d’API pour les intégrations (access_token). Le hasher est auto (argon2id), le form_login/json_login ont enable_csrf, la session est régénérée à la connexion. L’access_control est ordonné du plus spécifique au plus général et sert de première barrière (zones d’admin en ROLE_ADMIN) — mais l’équipe sait qu’il ne remplace pas l’autorisation par objet, assurée par des voters (chapitre suivant). Aucune route sensible n’est en security: false (réservé aux assets de dev).

✏️ Exercices

Exercice 1 — Répare l’access_control. L’ordre suivant laisse /admin/stats accessible à tout utilisateur connecté alors qu’il devrait être réservé aux admins. Pourquoi, et comment corriger ?

access_control: - { path: ^/admin, roles: ROLE_USER } - { path: ^/admin/stats, roles: ROLE_ADMIN }

✅ Solution

Symfony applique la première règle dont le path matche, puis s’arrête. Or ^/admin matche aussi /admin/stats : la première règle (ROLE_USER) est appliquée, et la seconde (ROLE_ADMIN) n’est jamais atteinte pour cette URL. Résultat : /admin/stats n’exige que ROLE_USER. Correctif : ordonner du plus spécifique au plus général :

access_control: - { path: ^/admin/stats, roles: ROLE_ADMIN } # specifique d'abord - { path: ^/admin, roles: ROLE_ADMIN } # (ou ROLE_USER selon le besoin) ensuite

Toujours placer les chemins précis avant les préfixes larges.

Exercice 2 — Le hasher de test. Un dev met password_hashers sur plaintext « juste pour débuguer la connexion en local ». Quels risques, et que faire ?

✅ Solution

Risques : (1) les mots de passe sont stockés en clair en base — une fuite les expose tous (et ils sont souvent réutilisés ailleurs) ; (2) ce réglage « de debug » finit en production (config commitée, oubli) — c’est un scénario récurrent d’incident. Il n’y a aucune bonne raison d’utiliser plaintext. Que faire : garder auto partout (argon2id), même en local — la vérification marche pareil. Pour débuguer, on inspecte le flux d’auth (logs, profiler), on ne dégrade jamais le stockage des mots de passe. Un hasher faible n’est pas un outil de debug, c’est une faille en attente.

🧠 Quiz de révision

1. Quel est le rôle d’un firewall dans Symfony ?

Il intercepte les requêtes d’une zone d’URL et décide comment elles sont authentifiées (via des authenticators : form login, json login, access token…). Il gère aussi le succès/échec, la session (régénérée à la connexion) et expose l’utilisateur courant. C’est la porte d’entrée de la sécurité d’accès.

2. Comment configure-t-on le hachage des mots de passe, et comment hache-t-on ?

Via password_hashers: 'auto' dans security.yaml (Symfony choisit argon2id/bcrypt, gère sel/coût/rehash). On hache avec UserPasswordHasherInterface::hashPassword($user, $plain) ; le firewall vérifie à la connexion (temps constant). On ne stocke jamais le clair, on ne compare jamais à la main.

3. Pourquoi activer enable_csrf sur le form login ?

Pour protéger le formulaire de connexion contre le CSRF (login CSRF, où un attaquant tenterait de connecter la victime à un compte contrôlé). C’est une protection intégrée qu’il suffit d’activer.

4. Quel est le piège de l’ordre dans access_control ?

Symfony applique la première règle dont le path matche puis s’arrête. Une règle large (^/admin) placée avant une plus spécifique (^/admin/stats) « avale » cette dernière, qui n’est jamais évaluée. Il faut ordonner du plus spécifique au plus général.

5. Pourquoi access_control ne suffit-il pas à sécuriser l’accès ?

Parce qu’il contrôle des zones d’URL (grossier), mais pas l’accès par objet : un utilisateur autorisé sur /facture peut demander la facture d’un autre (IDOR) si le contrôleur ne vérifie pas l’appartenance. Ce contrôle par objet relève des voters (chapitre 9.4).


Chapitre suivant : Voters & contrôle d’accès — l’autorisation par objet de la Partie 5, implémentée proprement en Symfony pour fermer les IDOR.

Last updated on