131% meer gemelde CVE's per dag in 2026 dan vorig jaar.

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

  1. Uw code Injection, XSS, deserialisatie, bestandsverwerking, sessies Standaardinstellingen van het framework, code review, PHPStan, Psalm, Semgrep
  2. Uw Composer-afhankelijkheden Bekende kwetsbaarheden, verlaten en kwaadaardige pakketten, lockfile-drift, code tijdens de installatie composer audit, Composer-policy's, Dependabot of Renovate
  3. 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
De drie lagen van PHP-beveiliging, en de tools voor elke laag

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 via php.ini, de FPM-pool of serverconfiguratie die geldt voordat het request wordt geparsed, en herstart elke worker. Vertrouw daarvoor niet op ini_set() of de .user.ini van 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
Ondersteunde PHP-versies (datums van php.net)

Productie-instellingen voor Laravel en Symfony

  • Debug uit. APP_DEBUG=false in Laravel, APP_ENV=prod en APP_DEBUG=0 in Symfony. Een debugpagina in productie toont configuratie- en omgevingswaarden.
  • Sleutels en geheimen. Houd APP_KEY (Laravel) en APP_SECRET (Symfony) buiten de repository en roteer ze als ze uitlekken. Met APP_PREVIOUS_KEYS van 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_proxies in Symfony), zodat een client zijn IP-adres of HTTPS niet kan vervalsen met X-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 $_GET en $_COOKIE naar sinks zoals echo, include en SQL-queries, en meldt problemen zoals TaintedSql en TaintedHtml. De huidige Psalm voert deze analyse uit samen met de typecontrole; --taint-analysis zet haar aan voor één run, en --report=results.sarif schrijft 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.lock voor applicaties, zodat elke omgeving dezelfde versies installeert.
  • Gebruik composer install in CI en deployments, nooit composer update, dat ter plekke nieuwe versies oplost.
  • Installeer productiebuilds met --no-dev, zodat testtooling nooit wordt meegeleverd.
  • Voer composer validate --strict uit: het faalt als composer.lock niet meer aansluit op composer.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-plugins elke 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 install met php artisan package:discover, dat de autoloader laadt. De autoloaded files van 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:

  1. Update het pakket dat de kwetsbare binnenhaalt. De meeste bevindingen zijn transitief; voer composer why vendor/package uit om de keten te zien, en update eerst het bovenliggende pakket binnen zijn constraint.
  2. Update één ding tegelijk. composer update vendor/package --with-dependencies houdt de wijziging klein genoeg om in de diff van composer.lock te beoordelen.
  3. Smoketest eerst de kritieke paden. Inloggen, afrekenen, de belangrijkste API-endpoints: een handvol end-to-endcontroles vangt de meeste problemen door upgrades op.
  4. Voeg characterization tests toe rond wat de upgrade raakt. Leg vast wat de code nu doet, zodat u merkt wanneer de upgrade dat verandert.
  5. 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: read voorkomt dat een gecompromitteerde stap code of releases pusht met GITHUB_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, draait composer install in elke job tegen de lockfile. Een herstelde vendor/ 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 audit en een gecommitte composer.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