Sécurité PHP & Symfony : OWASP Top 10, exemples concrets et scans CI/CD (Composer Audit, ZAP)
Cet article explique les failles web les plus exploitées (OWASP Top 10) et montre comment les éviter en PHP/Symfony avec des extraits concrets (requêtes préparées, escaping Twig, tokens CSRF, upload hors public, cookies sécurisés). Il va plus loin avec une approche "shift-left" en intégrant des contrôles automatiques dans la pipeline CI/CD : audit des dépendances (composer audit) et scan applicatif via OWASP ZAP baseline.
La sécurité applicative, on en parle souvent comme d’un “chantier à part”. Un audit annuel, un pentest à la fin, quelques recommandations et on coche la case. Sauf qu’en 2026, ce modèle ne tient plus. Une application PHP exposée sur Internet se fait scanner en continu, toute la journée, par des bots qui n’ont ni fatigue ni pitié. Et dans la vraie vie, l’incident n’arrive pas parce que quelqu’un a trouvé une faille géniale. Il arrive parce qu’on a laissé passer une faille classique.
L’OWASP Top 10 n’est pas une “liste marketing”. C’est la cartographie des erreurs qui reviennent partout, dans toutes les stacks, et qui provoquent des fuites de données, des détournements de comptes, des compromissions. L’idée de cet article est simple : te donner un guide complet, clair, concret, orienté PHP/Symfony, avec du vrai code, du vrai “comment ça se produit”, et surtout du “comment on évite ça proprement”.
Et à la fin, on passe au sujet qui change tout : comment automatiser ces contrôles dans la CI/CD, pour que la sécurité devienne une barrière naturelle avant la production, pas un discours après coup.
1) Broken Access Control - le moment où l’URL devient une clé universelle
C’est la faille numéro 1 pour une raison simple : elle arrive facilement, même dans une équipe expérimentée. Le scénario classique est celui-ci : tu as une page “Mon profil”, “Mes factures”, “Mon dossier”. Et techniquement, pour récupérer le bon objet, tu passes un identifiant. Tant que tout le monde clique dans l’interface, tout va bien. Le jour où quelqu’un modifie l’URL à la main, tu découvres si ton contrôle d’accès existe vraiment… ou s’il était implicite.
Exemple typique : un endpoint qui charge un utilisateur par ID sans vérifier que l’utilisateur connecté a le droit de le voir.
// ❌ Mauvais : on fait confiance à l’ID dans l’URL
public function show(Request $request, UserRepository $repo): Response
{
$id = (int) $request->query->get('id');
$user = $repo->find($id);
return $this->render('user/show.html.twig', ['user' => $user]);
}
Là, tu viens de créer une vulnérabilité de type IDOR (Insecure Direct Object Reference). Un utilisateur peut passer de ?id=12 à ?id=13 et lire le profil de quelqu’un d’autre. Et dans une app métier, “profil” peut vite vouloir dire : adresse, téléphone, documents, contrats, données bancaires…
La correction n’est pas “un if vite fait”. La correction robuste, c’est de rendre l’autorisation explicite, systématique, et centralisée. Dans Symfony, tu as exactement ce qu’il faut : voters, roles, attributs, politiques d’accès.
// ✅ Bon : contrôle explicite
public function show(User $user): Response
{
$this->denyAccessUnlessGranted('VIEW', $user);
return $this->render('user/show.html.twig', ['user' => $user]);
}
Le point important : on ne “devine” jamais les droits. On les vérifie. Et surtout, on n’encode pas une logique d’accès dans dix contrôleurs différents : on la met dans un Voter, et on la teste.
2) Cryptographic Failures - quand “ça marche” mais que c’est faible
Ici, l’OWASP vise tout ce qui touche à la protection des données sensibles : mots de passe, tokens, données personnelles, données bancaires, secrets, échanges réseau. La faille la plus fréquente, c’est le faux sentiment de sécurité. “On a hashé”, “on a chiffré”, “on est en HTTPS”. Oui… mais comment ? Avec quel algo ? Quel paramétrage ? Où sont les clés ? Combien de temps on garde les données ?
Le classique en PHP, c’est de stocker un mot de passe avec un hash faible ou fixe. Ou pire, de comparer des mots de passe en clair.
// ❌ Mauvais : hash faible ou maison
$hash = md5($password);
// ❌ Pire : stockage en clair
$user->setPassword($password);
En 2026, la règle est simple : password_hash() et rien d’autre. Et idéalement via le composant Security de Symfony (password hasher).
// ✅ Bon : hash moderne avec salt intégré
$hash = password_hash($password, PASSWORD_BCRYPT);
// ✅ Vérification
if (!password_verify($password, $hash)) {
throw new \RuntimeException('Bad credentials');
}
Et côté transport : oui, HTTPS, toujours. Mais aussi les cookies sécurisés et HttpOnly, sinon tu as du TLS… et un cookie qui part au premier XSS.
# config/packages/framework.yaml
framework:
session:
cookie_secure: auto
cookie_httponly: true
cookie_samesite: lax
Enfin, dernier point qui fait mal : les secrets. Une clé API committée dans Git, un token dans un log, un .env exposé. C’est rarement volontaire, mais c’est extrêmement fréquent. D’où l’intérêt d’un scanner de secrets dans la pipeline (on y revient plus bas).
3) Injection - SQL, commandes, LDAP : la donnée qui devient du code
L’injection, c’est le moment où une entrée utilisateur est interprétée comme une partie de ta requête ou de ta commande. SQL injection, command injection, LDAP injection… c’est la même famille. Tant que tu concatènes, tu prends un risque.
// ❌ SQL injection
$email = $request->query->get('email');
$sql = "SELECT * FROM users WHERE email = '$email'";
$rows = $pdo->query($sql)->fetchAll();
La correction robuste est connue, simple, et non négociable : requêtes préparées.
// ✅ Requête préparée
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);
$rows = $stmt->fetchAll();
Et attention à une variante souvent oubliée : l’injection via commande système. Il suffit d’un export PDF, d’un appel imagemagick, d’un grep, d’un convert, et tu as un risque d’exécution arbitraire.
// ❌ Command injection
$filename = $request->query->get('file');
exec("cat /var/data/" . $filename);
// ✅ Validation stricte + pas d’exec si possible
$filename = basename((string) $request->query->get('file'));
$path = '/var/data/' . $filename;
if (!is_file($path)) {
throw new \RuntimeException('Not found');
}
$content = file_get_contents($path);
La philosophie reste la même : ne pas exécuter, ne pas concaténer, valider strictement, et préférer des APIs haut niveau plutôt que des commandes shell.
4) Insecure Design - quand le produit est vulnérable même si le code est propre
Celui-là est sous-estimé. Beaucoup pensent que “sécurité = code”. En réalité, tu peux avoir un code impeccable, typé, testé… et une application totalement attaquable parce que le design fonctionnel autorise des abus.
Exemple : endpoint de login sans limitation de tentative. Pas besoin de hack, on brute-force. Simplement.
// ✅ Exemple simple : limiter les tentatives (principe)
if ($attemptsForIp > 10) {
throw new \RuntimeException('Too many attempts, try later');
}
En Symfony, tu peux faire bien mieux : rate limiter, firewall, stratégies. L’idée n’est pas juste de “bloquer” : c’est de concevoir des parcours robustes, avec des limites, des délais, des preuves (MFA), des contrôles de cohérence.
5) Security Misconfiguration - la faille la plus bête… et la plus rentable
Une config de debug activée, des headers absents, un serveur qui expose trop d’infos, des permissions de fichiers trop larges, un CORS permissif… Ce sont des failles “faciles” à exploiter, parce qu’elles n’exigent pas de casser ton code. Elles exploitent ton environnement.
Exemple : exposer des erreurs détaillées en production. On pense aider, mais on donne souvent des indices précieux à un attaquant (stack traces, chemins, versions).
# ✅ Prod : pas de debug, erreurs maîtrisées
APP_ENV=prod
APP_DEBUG=0
Autre point : les headers de sécurité. Ce n’est pas magique, mais c’est un socle. Et ça s’automatise (via reverse proxy, middleware, Symfony).
6) Vulnerable and Outdated Components - Composer, ton meilleur allié… et ton risque #1
Une app moderne PHP n’est pas “ton code”. C’est ton code + tout ce que Composer installe. Un projet peut avoir 30 dépendances directes, et 200 transitive. Une vulnérabilité critique dans une seule de ces librairies suffit à exposer l’app.
Ce point-là, tu ne peux pas le traiter “à la main”. Tu dois l’automatiser.
# ✅ Audit officiel Composer (à mettre dans la CI)
composer audit
Une bonne pratique de Tech Lead : définir une politique claire. Par exemple : “pas de vulnérabilité critique ou high en branche principale”. Et si c’est critique, la PR ne passe pas.
7) Identification and Authentication Failures - la session, c’est ton coffre-fort
Beaucoup d’applications “fonctionnent” avec une auth fragile. Sessions qui durent trop longtemps, cookies mal configurés, reset password trop permissif, pas de MFA sur les comptes sensibles, tokens réutilisables… Le problème, c’est que l’auth n’a pas besoin d’être cassée si on peut la contourner.
Exemple : token de reset password qui ne expire pas (ou qui expire trop tard), ou qui est réutilisable. Ça finit toujours par une prise de compte.
Les protections robustes : tokens courts, usage unique, expiration stricte, et journalisation des événements d’auth.
8) Software and Data Integrity Failures - la CI/CD comme vecteur d’attaque
Là, on touche un vrai sujet d’entreprise : la chaîne de build et de déploiement. Si quelqu’un injecte du code ou un package malveillant dans ta pipeline, tu peux déployer une compromission “officielle”, signée, sans alerte. C’est arrivé à des acteurs majeurs.
Concrètement : dépendances non verrouillées, downloads depuis des sources douteuses, secrets exposés dans les logs, artefacts non signés, droits trop larges sur les runners. La protection : verrouiller, signer, limiter, tracer.
# ✅ Utiliser le lockfile, builds reproductibles
composer install --no-interaction --prefer-dist --no-progress
9) Security Logging and Monitoring Failures - le jour où tu découvres trop tard
Une application peut être attaquée pendant des semaines sans que personne ne le voie, si tu n’as pas de logs utiles et d’alerting. Ici, l’OWASP ne dit pas “logguez tout”. Il dit : logguez ce qui compte, structurez, et alertez.
Exemples de choses qui doivent être visibles : tentatives de login échouées, changements de mot de passe, accès refusés, erreurs 403/401, pics de trafic, anomalies sur endpoints sensibles.
// ✅ Log utile et structuré
$logger->warning('Login failed', [
'email' => $email,
'ip' => $request->getClientIp(),
]);
10) SSRF - quand ton serveur devient un proxy vers l’interne
La SSRF arrive quand ton serveur fait des requêtes HTTP vers une URL contrôlée par l’utilisateur. Et c’est dangereux parce que ton serveur, lui, a accès à l’interne. À des métadonnées cloud, à des endpoints privés, à des backoffices internes.
// ❌ Très dangereux : URL user -> requête serveur
$url = $request->query->get('url');
$response = file_get_contents($url);
La protection robuste consiste à ne jamais laisser l’utilisateur définir une URL libre. On utilise une whitelist stricte de domaines, on résout l’IP, on bloque les ranges privés, et on impose des timeouts.
// ✅ Exemple : whitelist stricte
$allowedHosts = ['api.exemple.com', 'cdn.exemple.com'];
$parsed = parse_url((string) $url);
$host = $parsed['host'] ?? '';
if (!in_array($host, $allowedHosts, true)) {
throw new \RuntimeException('Host not allowed');
}
Comment automatiser le scan sécurité dans ta pipeline
La partie la plus importante, ce n’est pas de connaître la liste. C’est de ne pas laisser passer ces erreurs en production. Et pour ça, on traite la sécurité comme un contrôle qualité. Dans une pipeline CI/CD saine, tu as trois niveaux : scan dépendances, scan code, scan runtime.
Le premier niveau est non négociable : l’audit des dépendances Composer. C’est le garde-fou le plus rentable de tous, parce qu’il couvre une énorme surface d’attaque.
composer install
composer audit
Ensuite, tu ajoutes un scan statique (SAST) : PHPStan ou Psalm. Le but n’est pas seulement la qualité. C’est aussi de détecter des patterns dangereux, des concaténations suspectes, des flux de données non maîtrisés.
vendor/bin/phpstan analyse src --level=max
Enfin, tu ajoutes un scan dynamique (DAST) sur un environnement de dev/staging : OWASP ZAP va attaquer l’app comme un robot, et te sort un rapport.
docker run owasp/zap2docker-stable zap-baseline.py \
-t https://dev.monapp.com \
-r zap-report.html
Et là tu obtiens le mécanisme qui change tout : si un risque critique est détecté, la pipeline échoue. Pas de débat. Pas de “on verra plus tard”. On corrige, puis on merge.
C’est ça, un delivery robuste : pas juste “ça compile”, mais “ça tient”. Et c’est exactement ce qui différencie une équipe qui livre vite d’une équipe qui livre bien.
À retenir
OWASP Top 10 en PHP : comprendre, éviter, automatiser dans la CI/CD
Les vulnérabilités web les plus graves ne viennent pas d’attaques “magiques”, mais d’erreurs répétées : contrôle d’accès implicite, injections, XSS, mauvaises configurations, dépendances vulnérables, sessions fragiles, pipeline non verrouillée, manque de logs et SSRF. L’approche moderne consiste à les traiter comme un sujet de delivery : audits Composer automatisés, analyse statique (PHPStan/Psalm), tests, puis scan dynamique (OWASP ZAP) sur un environnement de staging. Résultat : on ne “corrige” plus après incident, on empêche les failles de passer.
Mots-clés : OWASP Top 10 PHP, sécurité Symfony, vulnérabilités web, injection SQL PHP, XSS Twig, CSRF Symfony, composer audit, DevSecOps PHP, OWASP ZAP, scan sécurité CI/CD, pipeline sécurisée, audit dépendances Composer.
Mostafa Nafi
Expert transformation digitale & architecte solutions
Mots-clés :
Vous travaillez sur un projet de transformation digitale, une architecture PHP/Symfony ou un SaaS ?
Si cet article fait écho à vos enjeux (modernisation de SI, scalabilité, APIs, organisation des équipes, micro-SaaS…), nous pouvons en discuter pour voir comment je peux vous aider concrètement.
Parler de votre projet