131% de CVE signalées en plus par jour en 2026 par rapport à l'an dernier.

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

  1. Votre code Injection, XSS, désérialisation, gestion des fichiers, sessions Valeurs par défaut du framework, revue de code, PHPStan, Psalm, Semgrep
  2. 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
  3. 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
Les trois couches de la sécurité PHP, et les outils qui agissent sur chacune

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 dans php.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 sur ini_set() ni sur le .user.ini de 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
Versions de PHP prises en charge (dates de php.net)

Réglages de production pour Laravel et Symfony

  • Débogage désactivé. APP_DEBUG=false dans Laravel, APP_ENV=prod et APP_DEBUG=0 dans Symfony. Une page de débogage en production affiche des valeurs de configuration et d'environnement.
  • Clés et secrets. Gardez APP_KEY (Laravel) et APP_SECRET (Symfony) hors du dépôt et renouvelez-les en cas de fuite. APP_PREVIOUS_KEYS de 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_proxies dans Symfony), afin qu'un client ne puisse pas falsifier son adresse IP ou HTTPS avec des en-têtes X-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 null non vérifié.
  • Psalm ajoute l'analyse de teinte : il suit les données depuis des sources comme $_GET et $_COOKIE jusqu'à des puits comme echo, include et les requêtes SQL, et signale des problèmes tels que TaintedSql et TaintedHtml. Les versions actuelles de Psalm l'exécutent avec la vérification de types ; --taint-analysis l'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.lock pour les applications, afin que chaque environnement installe les mêmes versions.
  • Utilisez composer install en CI et lors des déploiements, jamais composer 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 lorsque composer.lock n'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-plugins doit 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.json racine, 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 install se termine par php artisan package:discover, qui charge l'autoloader : les files autochargé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 :

  1. Mettez à jour le paquet qui tire la dépendance vulnérable. La plupart des résultats sont transitifs ; lancez composer why vendor/package pour voir la chaîne, et mettez d'abord à jour le paquet en amont dans le respect de sa contrainte.
  2. Mettez à jour une chose à la fois. composer update vendor/package --with-dependencies garde la modification assez petite pour être relue dans le diff de composer.lock.
  3. 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.
  4. 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.
  5. 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: read empêche une étape compromise de pousser du code ou des releases avec GITHUB_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 tourner composer install sur le lockfile dans chaque job. Un vendor/ 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 audit et un composer.lock commité.
  • 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