Sécurité PHP
Sécurité PHP : prévenir les vulnérabilités PHP dans votre code, vos dépendances Composer et votre build
Sécuriser PHP, c'est sécuriser trois couches. Dans le code que vous écrivez : validez les entrées, utilisez des requêtes paramétrées et l'échappement du framework, et ne désérialisez jamais de données non fiables. Dans les paquets Composer dont vous dépendez : exécutez composer audit, commitez composer.lock et bloquez les versions vulnérables. Dans la CI qui les installe : enregistrez ce qui a réellement été installé et limitez les destinations auxquelles le build peut se connecter. La plupart des guides de sécurité PHP s'arrêtent à la première couche. Celui-ci couvre les trois.
Mis à jour le
- Votre code Injection, XSS, désérialisation, gestion des fichiers, sessions Valeurs par défaut du framework, revue de code, PHPStan, Psalm, Semgrep
- Vos dépendances Composer Vulnérabilités connues, paquets abandonnés et malveillants, dérive du lockfile, code exécuté à l'installation composer audit, politiques Composer, Dependabot ou Renovate
- Votre build Ce que la CI a réellement installé et exécuté, d'où elle l'a récupéré, ce qu'elle pouvait joindre Actions épinglées, tokens à privilèges minimaux, politique de sortie réseau, SBOM issu du build
Sur cette page : PHP2Shell, les vulnérabilités PHP qui comptent encore, configuration et versions prises en charge, analyse statique, sécurité des dépendances Composer, attaques de la chaîne d'approvisionnement PHP, le build CI, SBOM et outils par couche.
PHP2Shell : la vulnérabilité PHP à corriger maintenant
Le 21 octobre 2026, le projet PHP a publié PHP 8.5.12, 8.4.27, 8.3.36 et 8.2.35 pour corriger deux vulnérabilités critiques dans PHP lui-même, qui peuvent permettre une exécution de code à distance sans authentification. Toutes deux sont notées 9,2 selon CVSS v4.0 et sont désignées ensemble sous le nom de PHP2Shell :
- CVE-2026-109618 : la gestion d'un manque de mémoire peut interrompre PHP alors que les données de la requête ne sont que partiellement initialisées, et le nettoyage de la requête peut alors traiter ces données invalides comme valides.
- CVE-2026-109619 : un état appartenant à une requête peut subsister après la libération de sa mémoire, et la requête suivante traitée par le même worker peut l'utiliser.
Selon l'avis de PHP, toute installation qui sert des requêtes web est exposée, notamment PHP-FPM, Apache avec mod_php, php-cgi, FrankenPHP et LiteSpeed, et mod_php présente le risque le plus élevé. PHP 8.1 et les versions antérieures ne reçoivent plus de nouvelle version du projet PHP. Vu la gravité, l'équipe de sécurité de PHP a toutefois publié des rétroportages minimaux des sources pour PHP 7.4, 8.0 et 8.1, destinés à ceux qui compilent PHP depuis les sources ou n'ont pas de fournisseur LTS, et certaines distributions et certains fournisseurs LTS livrent leurs propres correctifs rétroportés. Que faire :
- Passez à une version corrigée. En 8.1 ou plus ancien, passez à une branche prise en charge, installez le paquet rétroporté de votre fournisseur ou appliquez le rétroportage officiel des sources si vous compilez PHP vous-même.
-
Reconstruisez chaque image de conteneur qui embarque PHP et redéployez-la. Une ancienne étiquette d'image continue
d'exécuter l'ancien PHP tant qu'elle n'est pas remplacée : vérifiez donc ce qui est déployé, pas seulement ce qui se
trouve dans le dépôt :
docker run --rm IMAGE php -v. -
Si vous ne pouvez pas mettre à jour aujourd'hui, appliquez les mesures d'atténuation provisoires de l'avis de sécurité PHP, comme
session.upload_progress.enabled=0. Définissez-le dansphp.ini, dans le pool FPM ou dans une configuration serveur appliquée avant l'analyse de la requête, puis redémarrez chaque worker. Ne comptez pas surini_set()ni sur le.user.inide FPM pour cela : l'avis met en garde contre les deux. PHP est clair : ces mesures réduisent l'exposition mais ne rendent pas une installation sûre.
Notez ce que composer audit ne peut pas vous dire ici : PHP2Shell se trouve dans le runtime, pas dans un
paquet Composer, et n'apparaît donc jamais dans composer.lock. Le PHP que vous exécutez provient de votre
image de base ou des paquets de votre système d'exploitation, soit la troisième couche de ce guide. Pour le tableau des
versions, la chronologie et un script qui vérifie chaque étiquette d'image, consultez
PHP2Shell : quoi corriger et comment trouver chaque PHP vulnérable.
Les vulnérabilités PHP qui comptent encore
Les classes de vulnérabilités des applications PHP ont à peine changé en dix ans. Ce qui a changé, c'est que Laravel, Symfony et le PHP moderne en ferment la plupart par défaut : les vrais bugs se concentrent désormais là où le code contourne ces valeurs par défaut. L'injection occupe la place A05 du OWASP Top 10:2025, et Software Supply Chain Failures fait son entrée en A03 : c'est le sujet de la seconde moitié de ce guide.
| Vulnérabilité | Comment elle apparaît en PHP | La correction | Laravel et Symfony |
|---|---|---|---|
| Injection SQL | Données de requête concaténées dans le SQL, y compris via les échappatoires vers les requêtes brutes | Requêtes préparées PDO avec paramètres liés | Eloquent et Doctrine lient les paramètres ; whereRaw(), DB::raw() et le DQL concaténé ne le font pas |
| Cross-site scripting (XSS) | Saisie utilisateur renvoyée dans le HTML sans échappement | Échapper en sortie avec htmlspecialchars() ou le moteur de templates |
Blade {{ }} et Twig échappent par défaut ; {!! !!} et |raw non |
| Cross-site request forgery (CSRF) | Un formulaire ou un point d'accès AJAX qui modifie l'état accepte toute requête portant le cookie de session | Un jeton par session vérifié à chaque POST, PUT, PATCH et DELETE, plus des cookies SameSite |
Laravel vérifie le jeton sur les routes web (@csrf dans les formulaires) ; Symfony Forms en inclut et en vérifie un par défaut ; les routes exclues de la vérification n'ont aucune protection |
Injection de code via eval() |
Des données de requête, ou un modèle ou une formule modifiable par l'utilisateur, qui atteignent eval() |
Ne pas utiliser eval() ; associer l'entrée à un ensemble fixe d'opérations, ou utiliser un parseur pour les expressions nécessaires |
Aucun framework ne protège vos propres appels à eval() |
| Injection d'objets | unserialize() sur des cookies, des données de requête ou des entrées de cache qu'un attaquant peut écrire |
Utiliser JSON pour les données externes ; si vous devez désérialiser, refuser les objets | Aucun framework ne protège vos propres appels à unserialize() |
| Inclusion de fichiers et téléversements | Un chemin construit à partir d'une saisie et passé à include ; des fichiers téléversés stockés sous la racine web avec le nom fourni par le client |
Liste d'autorisation pour les fichiers inclus ; stocker les téléversements hors de la racine web sous des noms générés | store() de Laravel génère les noms de fichiers ; validez le type et la taille côté serveur |
| Type juggling | == pour comparer des tokens ou des hachages, alors que "0e123" == "0e456" est vrai |
===, hash_equals() pour les secrets, password_verify() pour les mots de passe |
Les helpers du framework comparent de façon sûre ; vos propres comparaisons relèvent de vous |
| Sessions et cookies faibles | Identifiants de session lisibles par JavaScript, envoyés en HTTP ou conservés après la connexion | Cookies Secure, HttpOnly et SameSite, mode strict, nouvel identifiant de session après la connexion | Les deux régénèrent l'identifiant de session à la connexion |
| Server-side request forgery (SSRF) | file_get_contents() ou cURL sur une URL fournie par l'utilisateur, y compris via des wrappers comme file:// et phar:// |
Liste d'autorisation pour les schémas et les hôtes, blocage des adresses internes, désactivation de allow_url_fopen s'il n'est pas utilisé |
Aucune protection par défaut |
| Injection de commandes | Saisie passée à exec(), shell_exec(), system() ou proc_open() |
Éviter le shell ; passer les arguments sous forme de tableau, ou échapper chacun avec escapeshellarg() |
Symfony Process et la façade Process de Laravel acceptent un tableau d'arguments |
| Mass assignment | User::create($request->all()) permet à une requête de définir is_admin |
Lister les champs remplissables, ou ne transmettre que des entrées validées | Les modèles Eloquent sont protégés par défaut ; $guarded = [] désactive cette protection |
Injection SQL : code vulnérable et corrigé
// Vulnerable: the id goes straight into the SQL string
$rows = $pdo->query("SELECT * FROM orders WHERE id = " . $_GET['id']);
// Fixed: a prepared statement with a bound parameter
$stmt = $pdo->prepare('SELECT * FROM orders WHERE id = ?');
$stmt->execute([$_GET['id']]);
$rows = $stmt->fetchAll();
// Laravel: whereRaw() takes SQL as written, where() binds the value
Order::whereRaw("status = '" . $request->input('status') . "'")->get(); // vulnerable
Order::where('status', $request->input('status'))->get(); // fixed
XSS : code vulnérable et corrigé
<?php // Plain PHP: escape on output
echo '<p>' . $comment . '</p>'; // vulnerable
echo '<p>' . htmlspecialchars($comment, ENT_QUOTES, 'UTF-8') . '</p>'; // fixed ?>
{{-- Blade: {{ }} escapes, {!! !!} prints raw HTML --}}
<p>{!! $comment !!}</p> {{-- vulnerable --}}
<p>{{ $comment }}</p> {{-- fixed --}}
{# Twig: autoescape is on, |raw turns it off #}
<p>{{ comment|raw }}</p> {# vulnerable #}
<p>{{ comment }}</p> {# fixed #}
Injection d'objets : code vulnérable et corrigé
// Vulnerable: an attacker-controlled cookie becomes PHP objects, and any
// class with a __wakeup() or __destruct() method in vendor/ is a gadget
$prefs = unserialize($_COOKIE['prefs']);
// Fixed: JSON carries data, not objects
$prefs = json_decode($_COOKIE['prefs'], true, 512, JSON_THROW_ON_ERROR);
// If you must read serialized data, refuse objects
$prefs = unserialize($data, ['allowed_classes' => false]);
La désérialisation Phar, la variante dans laquelle une opération sur un chemin phar:// désérialisait les
métadonnées de l'archive, a été restreinte dans PHP 8.0 : les métadonnées phar ne sont
plus désérialisées à l'ouverture
d'un phar, mais seulement lorsque le code appelle getMetadata(). Une raison de plus de laisser PHP 7 derrière
vous.
Inclusion de fichiers : code vulnérable et corrigé
// Vulnerable: ?page=../admin/reset reaches any .php file on the disk
include __DIR__ . '/pages/' . $_GET['page'] . '.php';
// Fixed: only names on an allow-list can be included
$pages = ['home' => 'home.php', 'pricing' => 'pricing.php'];
$file = $pages[$_GET['page'] ?? 'home'] ?? 'home.php';
include __DIR__ . '/pages/' . $file;
Durcissement de la configuration et du runtime
Une application sûre peut encore fuir par son runtime. Ces réglages de php.ini suivent
l'OWASP PHP
Configuration Cheat Sheet :
| Réglage | Valeur en production | Pourquoi |
|---|---|---|
display_errors |
Off (avec log_errors = On) |
Les traces d'appels révèlent des chemins, des requêtes et parfois des secrets |
expose_php |
Off |
Cesse d'annoncer la version exacte de PHP dans les en-têtes de réponse |
allow_url_include |
Off |
Empêche l'include d'URL distantes ; obsolète depuis PHP 7.4 |
allow_url_fopen |
Off si l'application ne récupère pas d'URL avec les fonctions de fichiers |
Réduit la SSRF via file_get_contents() |
disable_functions |
Par exemple exec, shell_exec, system, passthru, proc_open, popen |
Prive du shell un attaquant qui parvient à exécuter du code ; ne listez que ce que votre application n'appelle jamais |
open_basedir |
Le répertoire de l'application | Limite les chemins que les fonctions de fichiers de PHP peuvent ouvrir ; un garde-fou, pas une frontière d'isolation |
| Cookies de session | session.cookie_secure = 1, session.cookie_httponly = 1, session.cookie_samesite = Lax ou Strict, session.use_strict_mode = 1 |
Tient les identifiants de session à l'écart du HTTP en clair et de JavaScript, et rejette les identifiants que le serveur n'a pas émis |
file_uploads |
Off si l'application n'accepte aucun téléversement |
Supprime tout un point d'entrée |
Restez sur une version de PHP prise en charge
PHP offre à chaque branche deux ans de support actif, puis deux ans de correctifs de sécurité uniquement, selon php.net. PHP 8.1 est arrivé en fin de vie le 31 décembre 2025, et PHP 8.2 suivra le 31 décembre 2026. Une branche en fin de vie ne reçoit plus de nouvelle version du projet PHP, et c'est la situation dans laquelle PHP2Shell a placé les utilisateurs de 8.1 : leur seul correctif officiel était un rétroportage des sources.
- PHP 8.1 Fin de vie, terminé le 31 déc. 2025
- PHP 8.2 Correctifs de sécurité uniquement, jusqu'au 31 déc. 2026
- PHP 8.3 Correctifs de sécurité uniquement, jusqu'au 31 déc. 2027
- PHP 8.4 Support actif, jusqu'au 31 déc. 2026
- PHP 8.5 Support actif, jusqu'au 31 déc. 2027
Réglages de production pour Laravel et Symfony
-
Débogage désactivé.
APP_DEBUG=falsedans Laravel,APP_ENV=prodetAPP_DEBUG=0dans Symfony. Une page de débogage en production affiche des valeurs de configuration et d'environnement. -
Clés et secrets. Gardez
APP_KEY(Laravel) etAPP_SECRET(Symfony) hors du dépôt et renouvelez-les en cas de fuite.APP_PREVIOUS_KEYSde Laravel vous permet de renouveler la clé sans déconnecter tous les utilisateurs. -
Proxys de confiance. Derrière un load balancer, listez les proxys auxquels vous faites confiance (le
middleware trusted proxies de Laravel,
framework.trusted_proxiesdans Symfony), afin qu'un client ne puisse pas falsifier son adresse IP ou HTTPS avec des en-têtesX-Forwarded-*.
Analyse statique pour PHP (SAST)
L'analyse statique lit votre code sans l'exécuter. Trois outils open source couvrent la plupart des équipes PHP :
-
PHPStan détecte les erreurs de type et les bugs à des niveaux de rigueur croissants. Ce n'est pas un
scanner de sécurité, mais beaucoup de failles commencent par une confusion de types ou un
nullnon vérifié. -
Psalm ajoute l'analyse de
teinte : il suit les données depuis des sources comme
$_GETet$_COOKIEjusqu'à des puits commeecho,includeet les requêtes SQL, et signale des problèmes tels queTaintedSqletTaintedHtml. Les versions actuelles de Psalm l'exécutent avec la vérification de types ;--taint-analysisl'active pour une seule exécution, et--report=results.sarifécrit du SARIF pour GitHub code scanning. - Semgrep prend en charge PHP comme langage en disponibilité générale, avec des règles communautaires pour les erreurs PHP courantes et une analyse de flux de données inter-fonctions dans son offre commerciale.
Le SAST est bruyant sur du PHP legacy pour des raisons structurelles : includes dynamiques, variables variables, méthodes
magiques comme __get() et __call(), et conventions de framework qui font transiter les données
par des conteneurs et des façades que l'analyseur ne peut pas suivre. L'outil devine, et signale des faux positifs, ou
abandonne, et manque de vrais flux.
Sur une ancienne base de code, la meilleure adoption fonctionne comme un cliquet. Enregistrez les résultats actuels comme
référence (vendor/bin/phpstan analyse --generate-baseline écrit phpstan-baseline.neon ; Psalm
dispose de --set-baseline), ne faites échouer le build que sur les nouveaux résultats, et montez le niveau à
mesure que vous résorbez la référence. Des équipes qui maintiennent des bases de code PHP de longue date nous disent que la
correction automatique par IA fonctionne bien pour les mises à jour de dépendances et de façon inégale sur les résultats
liés au code : relisez donc les correctifs générés pour les résultats SAST avec autant de soin que n'importe quelle autre
modification. SAST et SCA répondent à des questions différentes ; voir SAST vs SCA.
Sécurité des dépendances Composer
Une application Laravel ou Symfony installe des dizaines de paquets Composer, la plupart transitifs. Composer a gagné de vraies fonctionnalités de sécurité au fil des dernières versions ; cette section porte sur Composer 2.11, la version actuelle.
composer audit
composer audit compare les paquets installés aux avis de sécurité de
l'API Packagist.org, qui collecte des
avis auprès de sources telles que la GitHub Advisory Database et FriendsOfPHP/security-advisories. Il signale aussi les
paquets abandonnés et, depuis Composer 2.10, les versions de paquets signalées comme malveillantes. Toutes les options
figurent dans la documentation de Composer.
composer audit # audit what is installed in vendor/
composer audit --locked # audit composer.lock, whatever is in vendor/
composer audit --no-dev # skip require-dev packages
composer audit --direct # direct dependencies only (Composer 2.11)
composer audit --format=json # table, plain, json or summary
composer audit --abandoned=report # ignore, report or fail on abandoned packages
composer audit --ignore-severity=low
Depuis Composer 2.10, le code de sortie vaut 0 en l'absence de problème et 1 dès qu'un élément
échoue à l'audit : il sert donc de contrôle en CI sans analyse de la sortie. Les paquets abandonnés font échouer l'audit
par défaut.
Les résultats acceptés ont leur place dans composer.json, avec une justification, pas dans une option de CI
dont plus personne ne se souvient. Composer 2.10 a remplacé l'essentiel de l'ancienne configuration audit
par un bloc policy :
{
"config": {
"policy": {
"advisories": {
"ignore-id": {
"CVE-1234": "Not affected: we never call the vulnerable parser."
}
},
"abandoned": {
"audit": "report"
}
}
}
}
Les anciennes clés (audit.ignore, audit.abandoned, audit.block-insecure)
fonctionnent encore en repli tant que la section policy correspondante est absente, mais elles sont
obsolètes. Migrez tous les réglages liés aux avis en une fois : dès que policy.advisories existe, toutes les
clés audit.* liées aux avis sont ignorées.
Bloquer les versions vulnérables
Depuis Composer 2.9 (novembre 2025), composer update et composer require refusent les versions
de paquets visées par un avis de sécurité. C'était le réglage audit.block-insecure, devenu
policy.advisories.block, activé par défaut. Composer 2.10 a ajouté le blocage des malwares à partir du flux
de malwares de Packagist, qui s'applique aussi à composer install. Composer 2.11 a ajouté une politique
cooldown facultative qui retient les versions nouvellement publiées pendant la durée de votre choix, par
exemple "7 days", car de nombreuses versions malveillantes sont détectées et retirées en quelques heures ou
quelques jours.
Deux détails comptent en CI. D'abord, le blocage par avis s'applique au moment de la résolution des versions : une version
déjà présente dans composer.lock s'installe toujours avec composer install après la
publication d'un avis, et composer audit reste donc nécessaire comme contrôle. Ensuite,
--no-blocking désactive toutes les politiques pour une commande : cela vaut la peine de le rechercher dans
vos pipelines.
Avant Composer 2.9, on bloquait généralement les versions vulnérables avec
roave/security-advisories, un paquet qui
déclare un conflict avec chaque version vulnérable connue, construit à partir de la GitHub Advisory Database
et de FriendsOfPHP. Il fonctionne toujours et reste utile sur les anciennes versions de Composer ; à partir de Composer 2.9,
le blocage intégré couvre les mêmes avis sans paquet supplémentaire.
Discipline du lockfile
- Commitez
composer.lockpour les applications, afin que chaque environnement installe les mêmes versions. -
Utilisez
composer installen CI et lors des déploiements, jamaiscomposer update, qui résout de nouvelles versions à la volée. - Installez les builds de production avec
--no-dev, pour que l'outillage de test ne soit jamais livré. -
Exécutez
composer validate --strict: il échoue lorsquecomposer.lockn'est plus à jour par rapport àcomposer.json.
Un lockfile enregistre ce que Composer a résolu. Voir ce qu'un lockfile n'enregistre pas.
Code exécuté à l'installation : plugins, scripts et fichiers autochargés
Composer exécute du code issu de vos dépendances à plus d'endroits que la plupart des équipes ne le pensent, ce qui en fait l'équivalent PHP des scripts d'installation npm :
-
Les plugins s'exécutent dans Composer lui-même. Depuis Composer 2.2, le réglage
allow-pluginsdoit lister chaque plugin autorisé à s'exécuter, et n'en autorise aucun par défaut. Traitez une nouvelle entrée comme une nouvelle dépendance dotée d'un accès shell. -
Les scripts ne s'exécutent que depuis votre
composer.jsonracine, pas depuis les dépendances. Mais ils démarrent souvent votre application. -
Les fichiers autochargés s'exécutent chaque fois que l'autoloader est chargé. Dans une application
Laravel standard,
composer installse termine parphp artisan package:discover, qui charge l'autoloader : lesfilesautochargés d'une dépendance s'exécutent donc dans votre job CI pendant l'installation.
Si un job n'a besoin ni de plugins ni de scripts, --no-plugins --no-scripts empêche le code des dépendances
de s'exécuter pendant l'installation ; lancez ensuite explicitement les étapes dont vous avez besoin.
Sources de dépôts et confusion de dépendances
Composer peut installer depuis Packagist.org, des dépôts Composer privés comme Private Packagist, des dépôts VCS (URL
Git) et des chemins locaux. Le mélange de sources privées et publiques est le terrain de la
confusion de dépendances : si le nom de votre paquet privé existe aussi sur
Packagist.org avec une version supérieure, lequel obtenez-vous ? Dans Composer 2,
chaque dépôt est
canonique par défaut : dès que Composer trouve un paquet dans un dépôt, il ne le cherche pas plus bas dans la liste,
et Packagist.org vient toujours en dernier. Définir "canonical": false sur un dépôt supprime cette
protection : ne le faites que délibérément. Les dépôts VCS récupèrent ce que l'hébergeur Git sert pour une étiquette :
épinglez-les par commit dans le lockfile et examinez leurs modifications.
Paquets abandonnés et non maintenus
Les parcs PHP legacy contiennent des paquets dont les mainteneurs sont passés à autre chose. Packagist marque un paquet
comme abandonné lorsque son mainteneur le déclare, souvent en indiquant un remplaçant, et composer audit
échoue alors par défaut. Beaucoup de paquets non maintenus ne sont toutefois jamais officiellement abandonnés. Regardez
la date de la dernière version de tout ce qui traite des entrées, l'authentification ou la cryptographie, et prévoyez des
remplacements avant qu'une vulnérabilité ne vous y oblige.
Mettre à jour sans risque quand les tests sont rares
Le cas le plus difficile est une grande application legacy avec une dépendance vulnérable et peu de tests. Ce qui fonctionne :
-
Mettez à jour le paquet qui tire la dépendance vulnérable. La plupart des résultats sont transitifs ;
lancez
composer why vendor/packagepour voir la chaîne, et mettez d'abord à jour le paquet en amont dans le respect de sa contrainte. -
Mettez à jour une chose à la fois.
composer update vendor/package --with-dependenciesgarde la modification assez petite pour être relue dans le diff decomposer.lock. - Testez d'abord les parcours critiques. Connexion, paiement, principaux endpoints de l'API : une poignée de vérifications de bout en bout détecte la plupart des régressions liées aux mises à jour.
- Ajoutez des tests de caractérisation autour de ce que la mise à jour touche. Enregistrez ce que fait le code aujourd'hui, pour remarquer quand la mise à jour le change.
- Déployez progressivement et surveillez les taux d'erreur avant que tous les clients ne soient concernés.
Ce que composer audit ne voit pas
Le lockfile est un plan, pas un enregistrement du build. composer audit lit vendor/ ou
composer.lock, et ne voit donc pas :
- PHP lui-même et ses extensions, qui proviennent de votre image de base ou des paquets du système (PHP2Shell en est un exemple).
- Les extensions PECL et les bibliothèques système installées dans l'image Docker.
- Les outils que votre CI récupère avec
curl, les actions qu'elle exécute et les binaires qu'un plugin ou un script télécharge. - Le code malveillant qui n'a encore ni avis ni signalement de malware.
Les mêmes limites s'appliquent à npm audit et à pip-audit dans leurs écosystèmes.
Attaques de la chaîne d'approvisionnement PHP
Quatre incidents montrent où les attaques de la chaîne d'approvisionnement contre PHP frappent réellement, et ce qui aurait permis de détecter chacune.
Mars 2021 : commits malveillants dans php-src
Le 28 mars 2021, deux commits malveillants ont été poussés dans le dépôt php-src sous le nom de deux développeurs du cœur. L'équipe PHP a annoncé qu'elle cessait d'exploiter son propre serveur Git et faisait des dépôts GitHub la référence canonique. Son suivi a établi que les commits avaient été poussés en HTTPS avec une authentification par mot de passe, peut-être avec des identifiants issus d'une base d'utilisateurs ayant fuité, et non via un serveur compromis. Ce qui l'aurait détecté : rien dans votre propre pipeline. L'attaque visait le langage lui-même, ce qui rappelle que le runtime PHP est lui aussi une dépendance.
Avril 2021 : injection de commandes dans Composer (CVE-2021-29472)
Une faille dans la gestion des URL de dépôts Mercurial par Composer permettait une injection de commandes. Packagist a indiqué que Packagist.org et Private Packagist étaient théoriquement exposés via des URL de paquets forgées, ne pensait pas qu'elle avait été exploitée, et l'a corrigée dans Composer 2.0.13 et 1.10.22. Ce qui l'aurait détecté : maintenir Composer lui-même à jour sur les postes des développeurs et dans les images de CI. Le gestionnaire de paquets fait partie de votre chaîne d'approvisionnement.
Mai 2023 : prises de contrôle de comptes Packagist
Le 1er mai 2023, un attaquant s'est connecté à quatre comptes Packagist.org inactifs avec des mots de passe ayant fuité
ailleurs, comptes qui contrôlaient ensemble 14 paquets, et a fait pointer ces paquets vers des dépôts forkés. Les forks
contenaient une description modifiée et aucun code malveillant.
Packagist
les a restaurés le lendemain matin. Ce qui l'aurait détecté : un composer.lock commité,
puisque composer install utilise les sources enregistrées dans le lockfile, et la relecture des diffs du
lockfile à la recherche d'URL inattendues, ce que Packagist recommande lui-même. Pour les mainteneurs : des mots de passe
uniques et l'authentification à deux facteurs.
2026 : étiquettes détournées sur laravel-lang et intercom-php
En 2026, des attaquants disposant d'accès volés ont publié des versions malveillantes de paquets existants. Packagist
cite intercom/intercom-php (30 avril) et les paquets laravel-lang (22 mai) dans sa
mise à jour
sur la sécurité de la chaîne d'approvisionnement. Dans le cas de laravel-lang, des étiquettes de version pointaient
vers des commits d'un fork malveillant, et la charge utile, un fichier src/helpers.php
chargé
par l'autoloader de Composer, récupérait un voleur d'identifiants ciblant les clés cloud, les clés SSH et les
fichiers .env. Aikido a compté 233 versions compromises. Ce qui l'aurait détecté : une fois
ces versions signalées, la politique anti-malware de Composer 2.10 les bloque même avec composer install ;
avant cela, une période de cooldown tient à l'écart les versions toutes récentes, une politique de sortie réseau empêche
la charge utile de joindre son serveur, et un enregistrement de chaque build montre quels builds ont installé une
mauvaise version.
Depuis, Packagist a annoncé qu'il rejette les versions stables réétiquetées, qu'il publie un journal de transparence des changements de propriétaire et d'étiquette, et qu'il signale les malwares sur Packagist.org depuis mars 2026.
Sécuriser le build PHP en CI
Le build est l'endroit où se rencontrent votre code, vos dépendances et vos secrets : c'est donc là qu'un paquet malveillant fait le plus de dégâts. Un workflow GitHub Actions pour un build de production :
name: build
on: [push, pull_request]
permissions:
contents: read # least privilege for GITHUB_TOKEN
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: shivammathur/setup-php@eb7c497e18156a6bbabfef1d3a82760b9eda3962 # 2.40.0
with:
php-version: "8.4"
tools: composer:v2
coverage: none
- run: composer validate --strict
- id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: composer-${{ hashFiles('composer.lock') }}
- run: composer install --no-dev --no-interaction --no-progress --optimize-autoloader
- run: composer audit --no-dev
-
Token à privilèges minimaux.
permissions: contents: readempêche une étape compromise de pousser du code ou des releases avecGITHUB_TOKEN. Voir les permissions du token GitHub. - Actions épinglées. Un SHA de commit complet ne peut pas être déplacé comme une étiquette ; le commentaire garde la version lisible pour Dependabot ou Renovate.
-
Mettez en cache les téléchargements, pas
vendor/. Mettre en cache le cache de téléchargement de Composer fait tournercomposer installsur le lockfile dans chaque job. Unvendor/restauré est accepté tel quel, et les caches peuvent être empoisonnés d'un workflow à l'autre ; voir le cache GitHub Actions. - Enregistrez ce qui a été installé. Le lockfile dit ce qui devrait être installé. Un SBOM enregistré pendant le build dit ce qui l'a été, y compris le runtime PHP, les paquets du système et les téléchargements hors de Composer. Voir les SBOM issus du build.
- Limitez les destinations auxquelles le build peut se connecter. Un build Composer a besoin de Packagist, de GitHub et de votre dépôt privé. Une politique de sortie réseau qui n'autorise que ceux-ci empêche un voleur d'identifiants d'envoyer ce qu'il a trouvé.
Générer un SBOM pour un projet PHP
L'option open source de référence est le
plugin CycloneDX pour Composer. Comme
il s'agit d'un plugin, il nécessite une entrée allow-plugins :
composer require --dev cyclonedx/cyclonedx-php-composer
composer CycloneDX:make-sbom --output-format=JSON --output-file=sbom.json
Il lit composer.json ainsi que les paquets installés ou composer.lock : il liste donc
précisément vos dépendances Composer, et rien d'autre. Le runtime PHP, ses extensions, les paquets apt ou apk de l'image
de base et les outils téléchargés par le job n'y figurent pas. Un SBOM issu du build les enregistre aussi, car il est
établi à partir de ce que le job a récupéré et non à partir des fichiers de Composer. Pour les formats et ce qu'un SBOM
doit contenir, voir qu'est-ce qu'un SBOM.
Outils de sécurité PHP par couche
| Couche | Outils | Ce qu'ils couvrent | Ce qu'ils manquent |
|---|---|---|---|
| Code | PHPStan, Psalm, Semgrep | Bugs et erreurs de type, flux de données contaminées, motifs non sécurisés connus | Dépendances vulnérables, runtime, build |
| Dépendances | composer audit, politiques Composer, roave/security-advisories, Dependabot ou Renovate |
Avis publiés, paquets abandonnés et signalés, pull requests de mise à jour | Votre propre code, et tout ce qui est hors de composer.lock |
| Image | Scanners de conteneurs | Paquets système et de langage dans une image construite | Ce que le build a récupéré sans le conserver, et les images que personne n'a scannées |
| SBOM | Plugin CycloneDX pour Composer | Les paquets Composer sous forme de SBOM CycloneDX | Tout ce qui est hors de Composer |
| Build | CRACI | Ce que chaque job GitHub Actions a réellement installé, depuis Composer et toute autre source, surveillé dans le temps | Votre code PHP (pas de SAST) ; les systèmes de CI autres que GitHub Actions |
Pour aller plus loin sur les images : l'analyse de conteneurs. Moins de résultats, mais de meilleurs, sur toutes les couches : comment réduire le bruit des vulnérabilités.
Sécurité PHP et CRACI
CRACI est un runner GitHub Actions qui enregistre ce que chaque job installe réellement. Remplacez runs-on
par craci, et chaque paquet Composer téléchargé par le job est enregistré avec son package URL
(pkg:composer/vendor/package), avec les paquets du gestionnaire de paquets du système, les images de
conteneurs, les sources Git et les autres téléchargements du même job, y compris les paquets restaurés depuis les caches
de CI. Chaque composant du SBOM était dans le build.
CRACI compare ces composants aux vulnérabilités connues et revérifie en continu les SBOM de ce que vous avez livré à mesure que de nouveaux avis sont publiés : une CVE divulguée l'an prochain vous mène toujours aux builds qui la contiennent. Les policy gates peuvent arrêter un build en cas de résultats, y compris un build qui contient une CVE précise, et la politique de sortie réseau limite les destinations auxquelles le job peut se connecter.
CRACI n'analyse pas votre code PHP : il n'y a pas de SAST, par choix. Conservez PHPStan, Psalm ou Semgrep pour la couche du code. Voyez chaque paquet que votre build PHP installe.
Quand CRACI n'est pas la réponse
-
Vous déployez par FTP ou sur un hébergement mutualisé sans CI. Il n'y a pas de build à enregistrer.
Commencez par
composer auditet uncomposer.lockcommité. - Votre risque se situe surtout dans votre propre code, par exemple une application legacy pleine de SQL brut. Le SAST, la revue et les tests passent en premier.
- Vous faites vos builds hors de GitHub Actions, sur GitLab CI ou Bitbucket Pipelines. CRACI prend en charge GitHub Actions aujourd'hui ; GitLab CI et Jenkins figurent sur la feuille de route.
Sécurité PHP : questions fréquentes
Comment sécuriser du code PHP ?
Travaillez sur trois couches. Dans votre code : validez les entrées, utilisez des requêtes préparées, appuyez-vous sur l'échappement en sortie du framework et ne désérialisez jamais de données non fiables. Dans vos dépendances : commitez composer.lock, exécutez composer audit en CI et laissez actif le blocage des versions vulnérables de Composer. Dans votre build : épinglez les actions, limitez le token, restreignez l'accès réseau et enregistrez ce que le job a installé.
Quelles sont les vulnérabilités PHP les plus courantes ?
L'injection SQL, le cross-site scripting, la cross-site request forgery (CSRF), l'injection de code via eval(), l'injection d'objets via unserialize(), l'inclusion de fichiers et les téléversements non sécurisés, le type juggling avec ==, des réglages de session faibles, la SSRF via les wrappers d'URL, l'injection de commandes et le mass assignment. Les dépendances Composer vulnérables et les versions de PHP non corrigées, comme avec les failles PHP2Shell, sont une cause d'incidents tout aussi fréquente.
Quel est le meilleur scanner de sécurité PHP ?
Aucun outil ne couvre à lui seul la sécurité PHP. Utilisez un analyseur statique comme PHPStan, Psalm ou Semgrep pour votre code, composer audit pour vos dépendances Composer, un scanner de conteneurs pour l'image, et un enregistrement issu du build pour ce que la CI a réellement installé.
Comment fonctionne composer audit, et que manque-t-il ?
Il compare les paquets installés, ou composer.lock avec --locked, aux avis de l'API Packagist.org, et signale les paquets abandonnés et ceux signalés comme malveillants. Il se termine avec le code 1 dès qu'un élément échoue à l'audit. Il ne voit ni PHP lui-même, ni les extensions PECL, ni les paquets système de votre image, ni rien de ce que la CI télécharge hors de Composer.
Comment bloquer les paquets Composer vulnérables en CI ?
Utilisez Composer 2.9 ou une version ultérieure, qui bloque par défaut les versions visées par un avis lors d'un update ou d'un require (policy.advisories.block). Comme une version vulnérable figée dans composer.lock s'installe quand même, exécutez aussi composer audit après composer install et faites échouer le job selon son code de sortie. Ne passez jamais --no-blocking en CI.
Laravel est-il sécurisé par défaut ?
En grande partie. Blade échappe la sortie, Eloquent lie les paramètres des requêtes, les modèles sont protégés contre le mass assignment et l'identifiant de session est régénéré à la connexion. Ces protections cessent de fonctionner quand le code utilise {!! !!}, whereRaw() avec une saisie, $guarded = [] ou APP_DEBUG=true en production, ou quand APP_KEY fuite.
Comment mettre à jour les dépendances d'une application PHP legacy peu testée ?
Mettez à jour le paquet en amont qui tire la dépendance vulnérable, un paquet à la fois avec composer update vendor/package --with-dependencies. Testez les parcours critiques, ajoutez des tests de caractérisation autour de ce que la mise à jour touche, et déployez progressivement.
Comment générer un SBOM pour un projet PHP ?
Le plugin CycloneDX pour Composer (cyclonedx/cyclonedx-php-composer) en génère un à partir de vos paquets Composer avec composer CycloneDX:make-sbom. Il ne liste que les paquets Composer. Un SBOM issu du build enregistre aussi le runtime PHP, les paquets système et les autres téléchargements du build.
Qu'est-ce que PHP2Shell ?
PHP2Shell désigne CVE-2026-109618 et CVE-2026-109619, deux vulnérabilités critiques dans PHP lui-même qui peuvent permettre une exécution de code à distance sans authentification. Elles sont corrigées dans PHP 8.5.12, 8.4.27, 8.3.36 et 8.2.35, publiées le 21 octobre 2026. Voir PHP2Shell.
Voyez chaque paquet que votre build PHP installe
Exécutez un workflow GitHub Actions sur CRACI et comparez son SBOM enregistré avec votre composer.lock.
Réserver une démo