PHP-beveiliging
PHP-beveiliging: PHP-kwetsbaarheden voorkomen in uw code, uw Composer-afhankelijkheden en uw build
PHP beveiligen betekent drie lagen beveiligen. In de code die u schrijft: valideer invoer, gebruik geparametriseerde queries en de escaping van het framework, en deserialiseer nooit onbetrouwbare data. In de Composer-pakketten waarvan u afhankelijk bent: voer composer audit uit, commit composer.lock en blokkeer onveilige versies. In de CI die ze installeert: leg vast wat er echt is geïnstalleerd en beperk waarmee de build verbinding mag maken. De meeste handleidingen over PHP-beveiliging stoppen bij de eerste laag. Deze behandelt alle drie.
Bijgewerkt op
- Uw code Injection, XSS, deserialisatie, bestandsverwerking, sessies Standaardinstellingen van het framework, code review, PHPStan, Psalm, Semgrep
- Uw Composer-afhankelijkheden Bekende kwetsbaarheden, verlaten en kwaadaardige pakketten, lockfile-drift, code tijdens de installatie composer audit, Composer-policy's, Dependabot of Renovate
- Uw build Wat de CI echt heeft geïnstalleerd en uitgevoerd, waar het vandaan kwam, wat de build kon bereiken Vastgepinde actions, tokens met minimale rechten, egress-policy, SBOM uit de build
Op deze pagina: PHP2Shell, de PHP-kwetsbaarheden die er nog toe doen, configuratie en ondersteunde versies, statische analyse, beveiliging van Composer-afhankelijkheden, supply chain-aanvallen op PHP, de CI-build, SBOM's en tools per laag.
PHP2Shell: de PHP-kwetsbaarheid die u nu moet patchen
Op 21 oktober 2026 bracht het PHP-project PHP 8.5.12, 8.4.27, 8.3.36 en 8.2.35 uit om twee kritieke kwetsbaarheden in PHP zelf te verhelpen die remote code execution zonder authenticatie mogelijk kunnen maken. Beide krijgen 9,2 onder CVSS v4.0 en samen staan ze bekend als PHP2Shell:
- CVE-2026-109618: de afhandeling van geheugentekort kan PHP onderbreken terwijl requestdata pas gedeeltelijk is geïnitialiseerd, waarna het opruimen van de request de ongeldige data als geldig kan behandelen.
- CVE-2026-109619: state die bij een request hoort, kan blijven bestaan nadat het geheugen ervan is vrijgegeven, en de volgende request die dezelfde worker afhandelt, kan die gebruiken.
Volgens de advisory van PHP loopt elke installatie die webrequests afhandelt risico, waaronder PHP-FPM, Apache met mod_php, php-cgi, FrankenPHP en LiteSpeed, en mod_php loopt het hoogste risico. PHP 8.1 en ouder krijgen geen nieuwe release van het PHP-project. Vanwege de ernst publiceerde het PHP-securityteam wel minimale source-backports voor PHP 7.4, 8.0 en 8.1, voor wie PHP uit de broncode bouwt of geen LTS-leverancier heeft, en sommige distributies en LTS-leveranciers leveren eigen gebackporte fixes. Wat u moet doen:
- Upgrade naar een gefixte release. Op 8.1 of ouder: stap over op een ondersteunde branch, neem het gebackporte pakket van uw leverancier, of pas de officiële source-backport toe als u PHP zelf bouwt.
-
Bouw elke container-image die PHP bevat opnieuw en deploy die opnieuw. Een oude image-tag blijft de oude PHP draaien
tot hij wordt vervangen, dus controleer wat er gedeployd is, niet alleen wat er in de repository staat:
docker run --rm IMAGE php -v. -
Kunt u vandaag niet upgraden, pas dan de tijdelijke maatregelen uit de PHP-beveiligingsadvisory toe, zoals
session.upload_progress.enabled=0. Stel dat in viaphp.ini, de FPM-pool of serverconfiguratie die geldt voordat het request wordt geparsed, en herstart elke worker. Vertrouw daarvoor niet opini_set()of de.user.inivan FPM: de advisory waarschuwt voor allebei. PHP is duidelijk: deze maatregelen verkleinen de blootstelling, maar maken een installatie niet veilig.
Let op wat composer audit u hier niet kan vertellen: PHP2Shell zit in de runtime, niet in een
Composer-pakket, en verschijnt dus nooit in composer.lock. De PHP die u draait komt uit uw base image of uw
OS-pakketten, de derde laag van deze handleiding. Voor de versietabel, de tijdlijn en een script dat elke image-tag
controleert, zie PHP2Shell: wat u moet patchen en hoe u elke kwetsbare PHP vindt.
De PHP-kwetsbaarheden die er nog toe doen
De kwetsbaarheidsklassen in PHP-applicaties zijn in tien jaar nauwelijks veranderd. Wat wel veranderde, is dat Laravel, Symfony en moderne PHP de meeste standaard dichten, zodat echte bugs zich nu concentreren op de plekken waar code om die standaarden heen werkt. Injection staat op A05 in de OWASP Top 10:2025, en Software Supply Chain Failures is nieuw op A03. Daarover gaat de tweede helft van deze handleiding.
| Kwetsbaarheid | Hoe die zich in PHP voordoet | De oplossing | Laravel en Symfony |
|---|---|---|---|
| SQL-injectie | Requestdata die in SQL wordt geplakt, ook via uitwijkmogelijkheden voor ruwe queries | PDO prepared statements met gebonden parameters | Eloquent en Doctrine binden parameters; whereRaw(), DB::raw() en aaneengeplakte DQL niet |
| Cross-site scripting (XSS) | Gebruikersinvoer die zonder escaping in HTML wordt teruggegeven | Escapen bij output met htmlspecialchars() of de template-engine |
Blade {{ }} en Twig escapen standaard; {!! !!} en |raw niet |
| Cross-site request forgery (CSRF) | Een formulier of AJAX-endpoint dat iets wijzigt, accepteert elk request dat de sessiecookie meestuurt | Een token per sessie, gecontroleerd bij elke POST, PUT, PATCH en DELETE, plus SameSite-cookies |
Laravel controleert het token op web-routes (@csrf in formulieren); Symfony Forms bevatten en controleren er standaard een; routes die van de controle zijn uitgezonderd, hebben geen bescherming |
Code-injectie via eval() |
Requestdata, of een template of formule die een gebruiker kan bewerken, die in eval() terechtkomt |
Gebruik geen eval(); koppel invoer aan een vaste set bewerkingen, of gebruik een parser voor de expressies die u nodig hebt |
Geen framework beschermt uw eigen eval()-aanroepen |
| Object injection | unserialize() op cookies, requestdata of cache-items die een aanvaller kan schrijven |
Gebruik JSON voor data van buiten; moet u deserialiseren, weiger dan objecten | Geen enkel framework beschermt uw eigen unserialize()-aanroepen |
| File inclusion en uploads | Een pad opgebouwd uit invoer dat aan include wordt doorgegeven; uploads opgeslagen onder de webroot met de bestandsnaam van de client |
Allowlist voor ingevoegde bestanden; sla uploads buiten de webroot op onder gegenereerde namen | store() van Laravel genereert bestandsnamen; valideer type en grootte op de server |
| Type juggling | == om tokens of hashes te vergelijken, waarbij "0e123" == "0e456" waar is |
===, hash_equals() voor geheimen, password_verify() voor wachtwoorden |
Frameworkhelpers vergelijken veilig; uw eigen vergelijkingen zijn uw verantwoordelijkheid |
| Zwakke sessies en cookies | Sessie-ID's die JavaScript kan lezen, die over HTTP worden verstuurd of na het inloggen behouden blijven | Secure-, HttpOnly- en SameSite-cookies, strict mode, een nieuw sessie-ID na het inloggen | Beide genereren bij het inloggen een nieuw sessie-ID |
| Server-side request forgery (SSRF) | file_get_contents() of cURL op een door de gebruiker opgegeven URL, inclusief wrappers zoals file:// en phar:// |
Allowlist voor schemes en hosts, blokkeer interne adressen, zet allow_url_fopen uit waar het niet wordt gebruikt |
Standaard geen bescherming |
| Command injection | Invoer die wordt doorgegeven aan exec(), shell_exec(), system() of proc_open() |
Vermijd de shell; geef argumenten door als array, of escape elk argument met escapeshellarg() |
Symfony Process en de Process-facade van Laravel accepteren een array met argumenten |
| Mass assignment | User::create($request->all()) laat een request is_admin instellen |
Benoem de invulbare velden, of geef alleen gevalideerde invoer door | Eloquent-modellen zijn standaard beschermd; $guarded = [] zet dat uit |
SQL-injectie: kwetsbaar en opgelost
// 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: kwetsbaar en opgelost
<?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 #}
Object injection: kwetsbaar en opgelost
// 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]);
Phar-deserialisatie, de variant waarbij een bestandsbewerking op een phar://-pad de metadata van het archief
deserialiseerde, is in PHP 8.0 ingeperkt: phar-metadata wordt
niet meer gedeserialiseerd bij het
openen van een phar, alleen wanneer code getMetadata() aanroept. Nog een reden om PHP 7 achter u te laten.
File inclusion: kwetsbaar en opgelost
// 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;
Configuratie en runtime harden
Ook een veilige applicatie kan via haar runtime lekken. Deze php.ini-instellingen volgen de
OWASP PHP
Configuration Cheat Sheet:
| Instelling | Waarde in productie | Waarom |
|---|---|---|
display_errors |
Off (met log_errors = On) |
Stack traces tonen paden, queries en soms geheimen |
expose_php |
Off |
Stopt het tonen van de exacte PHP-versie in response-headers |
allow_url_include |
Off |
Voorkomt include van externe URL's; deprecated sinds PHP 7.4 |
allow_url_fopen |
Off als de applicatie geen URL's ophaalt met bestandsfuncties |
Beperkt SSRF via file_get_contents() |
disable_functions |
Bijvoorbeeld exec, shell_exec, system, passthru, proc_open, popen |
Ontneemt een aanvaller die code kan uitvoeren de shell; vermeld alleen wat uw applicatie nooit aanroept |
open_basedir |
De applicatiemap | Beperkt de paden die PHP-bestandsfuncties kunnen openen; een vangrail, geen isolatiegrens |
| Sessiecookies | session.cookie_secure = 1, session.cookie_httponly = 1, session.cookie_samesite = Lax of Strict, session.use_strict_mode = 1 |
Houdt sessie-ID's weg van onversleuteld HTTP en van JavaScript, en weigert ID's die de server niet heeft uitgegeven |
file_uploads |
Off als de applicatie geen uploads accepteert |
Schakelt een volledig invoerpad uit |
Blijf op een ondersteunde PHP-versie
PHP geeft elke branch twee jaar actieve ondersteuning en daarna twee jaar alleen beveiligingsfixes, volgens php.net. PHP 8.1 bereikte op 31 december 2025 het einde van zijn levensduur, en PHP 8.2 volgt op 31 december 2026. Een branch die end-of-life is, krijgt geen nieuwe releases meer van het PHP-project, en in die positie bracht PHP2Shell de gebruikers van 8.1: hun enige officiële fix was een source-backport.
- PHP 8.1 End-of-life, beëindigd op 31 dec 2025
- PHP 8.2 Alleen beveiligingsfixes, tot 31 dec 2026
- PHP 8.3 Alleen beveiligingsfixes, tot 31 dec 2027
- PHP 8.4 Actieve ondersteuning, tot 31 dec 2026
- PHP 8.5 Actieve ondersteuning, tot 31 dec 2027
Productie-instellingen voor Laravel en Symfony
-
Debug uit.
APP_DEBUG=falsein Laravel,APP_ENV=prodenAPP_DEBUG=0in Symfony. Een debugpagina in productie toont configuratie- en omgevingswaarden. -
Sleutels en geheimen. Houd
APP_KEY(Laravel) enAPP_SECRET(Symfony) buiten de repository en roteer ze als ze uitlekken. MetAPP_PREVIOUS_KEYSvan Laravel roteert u de sleutel zonder alle gebruikers uit te loggen. -
Vertrouwde proxy's. Achter een load balancer vermeldt u de proxy's die u vertrouwt (de trusted
proxies-middleware van Laravel,
framework.trusted_proxiesin Symfony), zodat een client zijn IP-adres of HTTPS niet kan vervalsen metX-Forwarded-*-headers.
Statische analyse voor PHP (SAST)
Statische analyse leest uw code zonder die uit te voeren. Drie opensourcetools dekken de meeste PHP-teams af:
-
PHPStan vindt typefouten en bugs op steeds strengere niveaus. Het is geen beveiligingsscanner, maar
veel beveiligingsbugs beginnen als typeverwarring of een ongecontroleerde
null. -
Psalm voegt taint-analyse toe:
het volgt data van bronnen zoals
$_GETen$_COOKIEnaar sinks zoalsecho,includeen SQL-queries, en meldt problemen zoalsTaintedSqlenTaintedHtml. De huidige Psalm voert deze analyse uit samen met de typecontrole;--taint-analysiszet haar aan voor één run, en--report=results.sarifschrijft SARIF voor GitHub code scanning. - Semgrep ondersteunt PHP als algemeen beschikbare taal, met communityregels voor veelvoorkomende PHP-fouten en dataflow over functies heen in de commerciële versie.
SAST geeft op legacy-PHP om structurele redenen veel ruis: dynamische includes, variabele variabelen, magic methods zoals
__get() en __call(), en frameworkconventies die data door containers en facades leiden die de
analyzer niet kan volgen. De tool gokt, en meldt false positives, of geeft het op, en mist echte datastromen.
Op een oude codebase voert u het het best in als een ratel. Leg de huidige bevindingen vast als baseline
(vendor/bin/phpstan analyse --generate-baseline schrijft phpstan-baseline.neon; Psalm heeft
--set-baseline), laat de build alleen falen op nieuwe bevindingen, en verhoog het niveau naarmate u de
baseline afbouwt. Teams met langlevende PHP-codebases vertellen ons dat AI-autofix goed werkt voor updates van afhankelijkheden en
wisselend presteert bij codebevindingen. Beoordeel gegenereerde fixes voor SAST-bevindingen dus even zorgvuldig als elke
andere wijziging. SAST en SCA beantwoorden verschillende vragen; zie SAST vs SCA.
Beveiliging van Composer-afhankelijkheden
Een Laravel- of Symfony-applicatie installeert tientallen Composer-pakketten, de meeste transitief. Composer heeft in de laatste releases echte beveiligingsfuncties gekregen; deze sectie gaat uit van Composer 2.11, de huidige release.
composer audit
composer audit vergelijkt de geïnstalleerde pakketten met security advisories uit de
Packagist.org-API, die advisories
verzamelt uit bronnen zoals de GitHub Advisory Database en FriendsOfPHP/security-advisories. Het meldt ook verlaten
pakketten en, sinds Composer 2.10, pakketversies die als malware zijn gemarkeerd. Zie de
Composer-documentatie voor alle opties.
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
Sinds Composer 2.10 is de exitcode 0 als er geen problemen zijn en 1 als iets de audit niet
doorstaat, zodat het zonder parsen als CI-gate werkt. Verlaten pakketten laten de audit standaard falen.
Geaccepteerde bevindingen horen met een reden in composer.json, niet in een CI-flag die niemand zich
herinnert. Composer 2.10 verving het grootste deel van de oude audit-configuratie door een
policy-blok:
{
"config": {
"policy": {
"advisories": {
"ignore-id": {
"CVE-1234": "Not affected: we never call the vulnerable parser."
}
},
"abandoned": {
"audit": "report"
}
}
}
}
De oude sleutels (audit.ignore, audit.abandoned, audit.block-insecure) werken nog
als fallback zolang de bijbehorende policy-sectie ontbreekt, maar ze zijn deprecated. Migreer alle
advisory-instellingen in één keer: zodra policy.advisories bestaat, wordt elke advisory-gerelateerde
audit.*-sleutel genegeerd.
Onveilige versies blokkeren
Sinds Composer 2.9 (november 2025) weigeren composer update en composer require
pakketversies waarvoor een security advisory bestaat. Dat was de instelling audit.block-insecure en is nu
policy.advisories.block, standaard ingeschakeld. Composer 2.10 voegde het blokkeren van malware uit de
malwarefeed van Packagist toe, wat ook voor composer install geldt. Composer 2.11 voegde een optionele
cooldown-policy toe die nieuw uitgebrachte versies tegenhoudt gedurende een periode die u kiest, zoals
"7 days", omdat veel kwaadaardige versies binnen uren of dagen worden ontdekt en verwijderd.
In CI zijn twee details belangrijk. Ten eerste geldt het blokkeren op basis van advisories wanneer versies worden
opgelost: een versie die al in composer.lock staat, wordt met composer install nog steeds
geïnstalleerd nadat een advisory is gepubliceerd, dus u hebt composer audit nog altijd nodig als gate. Ten
tweede zet --no-blocking elke policy uit voor één commando; het loont om uw pipelines daarop te doorzoeken.
Vóór Composer 2.9 blokkeerde u onveilige versies meestal met
roave/security-advisories, een pakket dat een
conflict declareert met elke bekende kwetsbare versie, opgebouwd uit de GitHub Advisory Database en
FriendsOfPHP. Het werkt nog steeds en helpt nog op oudere Composer-versies; vanaf Composer 2.9 dekt de ingebouwde
blokkering dezelfde advisories zonder het extra pakket.
Lockfile-discipline
- Commit
composer.lockvoor applicaties, zodat elke omgeving dezelfde versies installeert. -
Gebruik
composer installin CI en deployments, nooitcomposer update, dat ter plekke nieuwe versies oplost. - Installeer productiebuilds met
--no-dev, zodat testtooling nooit wordt meegeleverd. -
Voer
composer validate --strictuit: het faalt alscomposer.lockniet meer aansluit opcomposer.json.
Een lockfile legt vast wat Composer heeft opgelost. Zie wat een lockfile niet vastlegt.
Code tijdens de installatie: plugins, scripts en autoloaded bestanden
Composer voert op meer plekken code uit uw afhankelijkheden uit dan de meeste teams verwachten. Daarmee is het de PHP-tegenhanger van npm-installatiescripts:
-
Plugins draaien binnen Composer zelf. Sinds Composer 2.2 moet de instelling
allow-pluginselke plugin vermelden die mag draaien, en standaard staat ze er geen toe. Behandel een nieuwe vermelding daar als een nieuwe afhankelijkheid met shelltoegang. -
Scripts draaien alleen vanuit uw root-
composer.json, niet vanuit afhankelijkheden. Maar ze starten vaak uw applicatie op. -
Autoloaded bestanden draaien zodra de autoloader wordt geladen. In een standaard Laravel-applicatie
eindigt
composer installmetphp artisan package:discover, dat de autoloader laadt. De autoloadedfilesvan een afhankelijkheid worden dus tijdens de installatie in uw CI-job uitgevoerd.
Heeft een job geen plugins of scripts nodig, dan voorkomt --no-plugins --no-scripts dat code uit afhankelijkheden
tijdens de installatie draait; voer de stappen die u nodig hebt daarna expliciet uit.
Repositorybronnen en dependency confusion
Composer kan installeren vanaf Packagist.org, private Composer-repository's zoals Private Packagist, VCS-repository's
(Git-URL's) en lokale paden. Waar private en publieke bronnen worden gemengd, ontstaat
dependency confusion: als de naam van uw private pakket ook op Packagist.org bestaat
met een hogere versie, welke krijgt u dan? In Composer 2 is
elke repository
canoniek, standaard: zodra Composer een pakket in een repository vindt, zoekt het er niet verder naar lager in de
lijst, en Packagist.org staat altijd als laatste. "canonical": false instellen op een repository haalt die
bescherming weg, dus doe dat alleen bewust. VCS-repository's leveren wat de Git-host voor een tag serveert, dus pin ze
op commit in de lockfile en beoordeel wijzigingen eraan.
Verlaten en niet-onderhouden pakketten
Legacy-PHP-omgevingen bevatten pakketten waarvan de maintainers verder zijn gegaan. Packagist markeert een pakket als
verlaten wanneer de maintainer dat aangeeft, vaak met een vervanger, en composer audit faalt daar standaard
op. Veel niet-onderhouden pakketten worden echter nooit formeel verlaten. Kijk naar de datum van de laatste release van
alles wat invoer, authenticatie of cryptografie verwerkt, en plan vervangingen voordat een kwetsbaarheid u ertoe dwingt.
Veilig updaten als er weinig tests zijn
Het lastigste geval is een grote legacy-applicatie met een kwetsbare afhankelijkheid en weinig tests. Wat werkt:
-
Update het pakket dat de kwetsbare binnenhaalt. De meeste bevindingen zijn transitief; voer
composer why vendor/packageuit om de keten te zien, en update eerst het bovenliggende pakket binnen zijn constraint. -
Update één ding tegelijk.
composer update vendor/package --with-dependencieshoudt de wijziging klein genoeg om in de diff vancomposer.lockte beoordelen. - Smoketest eerst de kritieke paden. Inloggen, afrekenen, de belangrijkste API-endpoints: een handvol end-to-endcontroles vangt de meeste problemen door upgrades op.
- Voeg characterization tests toe rond wat de upgrade raakt. Leg vast wat de code nu doet, zodat u merkt wanneer de upgrade dat verandert.
- Rol gefaseerd uit en houd de foutpercentages in de gaten voordat het elke klant bereikt.
Wat composer audit mist
De lockfile is een plan, geen registratie van de build. composer audit leest vendor/ of
composer.lock, en ziet daardoor niet:
- PHP zelf en zijn extensies, die uit uw base image of OS-pakketten komen (PHP2Shell is een voorbeeld).
- PECL-extensies en systeembibliotheken die in de Docker-image zijn geïnstalleerd.
- Tools die uw CI ophaalt met
curl, actions die het uitvoert, en binaries die een plugin of script downloadt. - Kwaadaardige code waarvoor nog geen advisory of malwaremarkering bestaat.
Dezelfde beperkingen gelden voor npm audit en pip-audit in hun eigen ecosystemen.
Supply chain-aanvallen op PHP
Vier incidenten laten zien waar supply chain-aanvallen op PHP echt toeslaan, en wat elk ervan had opgemerkt.
Maart 2021: kwaadaardige commits in php-src
Op 28 maart 2021 werden twee kwaadaardige commits naar de php-src-repository gepusht onder de namen van twee core-ontwikkelaars. Het PHP-team kondigde aan dat het zou stoppen met een eigen Git-server en de GitHub-repository's canoniek zou maken. Uit de follow-up bleek dat de commits via HTTPS met wachtwoordauthenticatie waren gepusht, mogelijk met inloggegevens uit een gelekte gebruikersdatabase, en niet via een gecompromitteerde server. Wat het had opgemerkt: niets in uw eigen pipeline. De aanval richtte zich op de taal zelf, een herinnering dat ook de PHP-runtime een afhankelijkheid is.
April 2021: command injection in Composer (CVE-2021-29472)
Een fout in de manier waarop Composer URL's van Mercurial-repository's verwerkte, maakte command injection mogelijk. Packagist meldde dat Packagist.org en Private Packagist theoretisch kwetsbaar waren via gemanipuleerde pakket-URL's, ging er niet van uit dat de fout was misbruikt, en verhielp die in Composer 2.0.13 en 1.10.22. Wat het had opgemerkt: Composer zelf up-to-date houden op ontwikkelaarsmachines en in CI-images. De pakketbeheerder is onderdeel van uw supply chain.
Mei 2023: overname van Packagist-accounts
Op 1 mei 2023 logde een aanvaller in op vier inactieve Packagist.org-accounts met elders gelekte wachtwoorden. Samen
beheerden die accounts 14 pakketten, en de aanvaller liet de pakketten naar geforkte repository's verwijzen. De forks
bevatten een gewijzigde beschrijving en geen kwaadaardige code.
Packagist
herstelde ze de volgende ochtend. Wat het had opgemerkt: een gecommitte composer.lock,
omdat composer install de bronnen gebruikt die in de lockfile zijn vastgelegd, en lockfile-diffs nakijken op
onverwachte URL's, wat Packagist zelf aanraadt. Voor maintainers: unieke wachtwoorden en tweefactorauthenticatie.
2026: gekaapte tags bij laravel-lang en intercom-php
In 2026 publiceerden aanvallers met gestolen toegang kwaadaardige releases van bestaande pakketten. Packagist noemt
intercom/intercom-php (30 april) en de laravel-lang-pakketten (22 mei) in zijn
update over
supply chain-beveiliging. Bij laravel-lang wezen versietags naar commits in een kwaadaardige fork, en de payload, een
src/helpers.php-bestand dat
door
de autoloader van Composer werd geladen, haalde een credential stealer op voor cloudsleutels, SSH-sleutels en
.env-bestanden. Aikido telde 233 gecompromitteerde versies. Wat het had opgemerkt: zodra ze
gemarkeerd zijn, blokkeert de malwarepolicy van Composer 2.10 die versies ook bij composer install; daarvoor
houdt een cooldownperiode gloednieuwe versies buiten, voorkomt een egress-policy dat de payload zijn server bereikt, en
laat een registratie van elke build zien welke builds een slechte versie hebben geïnstalleerd.
Sindsdien heeft Packagist aangekondigd dat het opnieuw getagde stabiele versies weigert, een transparantielog van eigendoms- en tagwijzigingen publiceert, en sinds maart 2026 malware op Packagist.org markeert.
De PHP-build in CI beveiligen
In de build komen uw code, uw afhankelijkheden en uw geheimen samen. Daar richt een kwaadaardig pakket dus de meeste schade aan. Een GitHub Actions-workflow voor een productiebuild:
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 met minimale rechten.
permissions: contents: readvoorkomt dat een gecompromitteerde stap code of releases pusht metGITHUB_TOKEN. Zie GitHub-tokenrechten. - Vastgepinde actions. Een volledige commit-SHA kan niet worden verplaatst zoals een tag; het commentaar houdt de versie leesbaar voor Dependabot of Renovate.
-
Cache downloads, niet
vendor/. Door de downloadcache van Composer te cachen, draaitcomposer installin elke job tegen de lockfile. Een hersteldevendor/wordt vertrouwd zoals hij is, en caches kunnen over workflows heen worden vergiftigd; zie GitHub Actions-cache. - Leg vast wat er is geïnstalleerd. De lockfile zegt wat er geïnstalleerd zou moeten worden. Een SBOM die tijdens de build is vastgelegd, zegt wat er geïnstalleerd is, inclusief de PHP-runtime, OS-pakketten en downloads buiten Composer. Zie SBOM's uit de build.
- Beperk waarmee de build verbinding mag maken. Een Composer-build heeft Packagist, GitHub en uw private repository nodig. Een egress-policy die alleen die toestaat, voorkomt dat een credential stealer verstuurt wat hij heeft gevonden.
Een SBOM genereren voor een PHP-project
De standaard opensourceoptie is de
CycloneDX Composer-plugin. Als plugin
heeft hij een vermelding in allow-plugins nodig:
composer require --dev cyclonedx/cyclonedx-php-composer
composer CycloneDX:make-sbom --output-format=JSON --output-file=sbom.json
Hij leest composer.json samen met de geïnstalleerde pakketten of composer.lock, en somt uw
Composer-afhankelijkheden dus nauwkeurig op, en verder niets. De PHP-runtime, zijn extensies, de apt- of apk-pakketten van de
base image en de tools die de job heeft gedownload, staan er niet in. Een SBOM uit de build legt ook die vast, omdat hij
is opgesteld op basis van wat de job heeft opgehaald en niet op basis van de bestanden van Composer. Voor de formaten en
wat een SBOM moet bevatten, zie wat is een SBOM.
PHP-beveiligingstools per laag
| Laag | Tools | Wat ze dekken | Wat ze missen |
|---|---|---|---|
| Code | PHPStan, Psalm, Semgrep | Bugs en typefouten, besmette datastromen, bekende onveilige patronen | Kwetsbare afhankelijkheden, de runtime, de build |
| Afhankelijkheden | composer audit, Composer-policy's, roave/security-advisories, Dependabot of Renovate |
Gepubliceerde advisories, verlaten en gemarkeerde pakketten, update-pull-requests | Uw eigen code, en alles buiten composer.lock |
| Image | Containerscanners | OS- en taalpakketten in een gebouwde image | Wat de build heeft opgehaald maar niet bewaard, en images die niemand heeft gescand |
| SBOM | CycloneDX Composer-plugin | Composer-pakketten als CycloneDX-SBOM | Alles buiten Composer |
| Build | CRACI | Wat elke GitHub Actions-job echt heeft geïnstalleerd, uit Composer en elke andere bron, doorlopend gemonitord | Uw PHP-code (geen SAST); andere CI-systemen dan GitHub Actions |
Meer over images: containerscanning. Minder en betere bevindingen over alle lagen heen: zo vermindert u ruis in kwetsbaarheidsmeldingen.
PHP-beveiliging en CRACI
CRACI is een GitHub Actions-runner die vastlegt wat elke job echt installeert. Verander runs-on in
craci, en elk Composer-pakket dat de job downloadt, wordt vastgelegd met zijn pakket-URL
(pkg:composer/vendor/package), samen met pakketten uit de pakketbeheerder van het OS, container-images,
Git-bronnen en andere downloads in dezelfde job, inclusief pakketten die uit CI-caches zijn hersteld. Elk component in de
SBOM zat in de build.
CRACI vergelijkt die componenten met bekende kwetsbaarheden en blijft de SBOM's van wat u hebt uitgeleverd opnieuw controleren zodra er nieuwe advisories verschijnen. Zo leidt een CVE die volgend jaar bekend wordt u nog steeds naar de builds die hem bevatten. Policy gates kunnen een build stoppen bij bevindingen, ook een build die een specifieke CVE bevat, en de egress-policy beperkt waarmee de job verbinding kan maken.
CRACI scant uw PHP-code niet: er is bewust geen SAST. Houd PHPStan, Psalm of Semgrep voor de codelaag. Bekijk elk pakket dat uw PHP-build installeert.
Wanneer CRACI niet het antwoord is
-
U deployt via FTP of naar shared hosting zonder CI. Er is geen build om vast te leggen. Begin met
composer auditen een gecommittecomposer.lock. - Uw risico zit vooral in uw eigen code, zoals een legacy-applicatie vol ruwe SQL. SAST, review en tests komen eerst.
- U bouwt buiten GitHub Actions, op GitLab CI of Bitbucket Pipelines. CRACI ondersteunt vandaag GitHub Actions; GitLab CI en Jenkins staan op de roadmap.
PHP-beveiliging: veelgestelde vragen
Hoe beveilig ik PHP-code?
Werk in drie lagen. In uw code: valideer invoer, gebruik prepared statements, vertrouw op de output-escaping van het framework en deserialiseer nooit onbetrouwbare data. In uw afhankelijkheden: commit composer.lock, voer composer audit uit in CI en laat de blokkering van onveilige versies in Composer aan staan. In uw build: pin actions vast, beperk het token, beperk netwerktoegang en leg vast wat de job heeft geïnstalleerd.
Wat zijn de meest voorkomende PHP-kwetsbaarheden?
SQL-injectie, cross-site scripting, cross-site request forgery, code-injectie via eval(), object injection via unserialize(), file inclusion en onveilige uploads, type juggling met ==, zwakke sessie-instellingen, SSRF via URL-wrappers, command injection en mass assignment. Kwetsbare Composer-afhankelijkheden en ongepatchte PHP-versies, zoals bij de PHP2Shell-bugs, zijn een even vaak voorkomende oorzaak van incidenten.
Wat is de beste PHP-beveiligingsscanner?
Geen enkele tool dekt PHP-beveiliging in zijn geheel. Gebruik een statische analyzer zoals PHPStan, Psalm of Semgrep voor uw code, composer audit voor uw Composer-afhankelijkheden, een containerscanner voor de image, en een registratie uit de build voor wat de CI echt heeft geïnstalleerd.
Hoe werkt composer audit, en wat mist het?
Het vergelijkt geïnstalleerde pakketten, of composer.lock met --locked, met advisories uit de Packagist.org-API, en meldt verlaten en als malware gemarkeerde pakketten. Het eindigt met 1 als iets de audit niet doorstaat. Het ziet PHP zelf niet, en ook geen PECL-extensies, OS-pakketten in uw image of iets wat de CI buiten Composer downloadt.
Hoe blokkeer ik onveilige Composer-pakketten in CI?
Gebruik Composer 2.9 of nieuwer, dat versies met advisories standaard blokkeert bij update en require (policy.advisories.block). Omdat een in composer.lock vastgelegde kwetsbare versie toch wordt geïnstalleerd, voert u ook composer audit uit na composer install en laat u de job falen op de exitcode. Geef in CI nooit --no-blocking mee.
Is Laravel standaard veilig?
Grotendeels. Blade escapet output, Eloquent bindt queryparameters, modellen zijn beschermd tegen mass assignment en het sessie-ID wordt bij het inloggen vernieuwd. Die standaarden werken niet meer wanneer code {!! !!}, whereRaw() met invoer, $guarded = [] of APP_DEBUG=true in productie gebruikt, of wanneer APP_KEY uitlekt.
Hoe update ik afhankelijkheden in een legacy-PHP-app met weinig tests?
Update het bovenliggende pakket dat de kwetsbare binnenhaalt, één pakket tegelijk met composer update vendor/package --with-dependencies. Smoketest de kritieke paden, voeg characterization tests toe rond wat de upgrade raakt, en rol gefaseerd uit.
Hoe genereer ik een SBOM voor een PHP-project?
De CycloneDX Composer-plugin (cyclonedx/cyclonedx-php-composer) genereert er een uit uw Composer-pakketten met composer CycloneDX:make-sbom. Hij somt alleen Composer-pakketten op. Een SBOM uit de build legt ook de PHP-runtime, OS-pakketten en andere downloads in de build vast.
Wat is PHP2Shell?
PHP2Shell is de naam voor CVE-2026-109618 en CVE-2026-109619, twee kritieke kwetsbaarheden in PHP zelf die remote code execution zonder authenticatie mogelijk kunnen maken. Ze zijn verholpen in PHP 8.5.12, 8.4.27, 8.3.36 en 8.2.35, uitgebracht op 21 oktober 2026. Zie PHP2Shell.
Bekijk elk pakket dat uw PHP-build installeert
Draai één GitHub Actions-workflow op CRACI en vergelijk de vastgelegde SBOM met uw composer.lock.
Demo boeken