Actuele situatie: PHP2Shell. Twee kritieke PHP-kwetsbaarheden (CVSS 9.2) maken schadelijke code zonder inloggen mogelijk. Patches sinds 21-10-2026.Wat u nu moet doen →

Hoe PHP bestandsuploads verwerkt

Tussen de klik op Uploaden en uw code ligt in PHP een hele verwerkingsketen. Deze pagina legt multipart/form-data, tijdelijke bestanden, $_FILES, limieten en het verband met PHP2Shell uit.

Stand 5 min. leestijdPHP-kennis

Kort gezegd: Wanneer een browser een bestand uploadt, stuurt hij een POST-verzoek in het formaat multipart/form-data, waarin formuliervelden en bestandsinhoud als afzonderlijke delen achter elkaar staan. PHP ontleedt dit verzoek zelf, schrijft elk bestandsdeel naar een tijdelijk bestand in upload_tmp_dir, legt de gegevens vast in $_FILES en start pas daarna uw script. Het script verplaatst het bestand met move_uploaded_file(); alles wat niet wordt verplaatst, verwijdert PHP aan het einde van het verzoek.

De weg van een bestand van de browser naar het script

Voor beslissers is vooral één ding belangrijk: een upload is niet alleen een zaak van de toepassing. Een groot deel van het werk gebeurt in de PHP-interpreter, voordat de code van het CMS of de webshop er überhaupt bij betrokken is. Daarom raken fouten in dit deel van PHP elke website, ongeacht of er een uploadformulier is.

Het verloop in het kort:

  1. Het formulier met method="POST" en enctype="multipart/form-data" wordt verzonden.
  2. De webserver accepteert het verzoek en geeft het door aan PHP, afhankelijk van de werkwijze via PHP-FPM, mod_php of een andere interface.
  3. PHP controleert limieten zoals post_max_size en splitst de inhoud op aan de hand van een scheidingsreeks, de zogeheten boundary.
  4. Gewone velden komen in $_POST terecht, bestandsdelen worden naar tijdelijke bestanden geschreven en in $_FILES beschreven.
  5. Pas nu start het script, controleert het bestand en verplaatst het naar de bestemming.
  6. Aan het einde van het verzoek verwijdert PHP alle tijdelijke bestanden die niet zijn verplaatst.

multipart/form-data: de opbouw van een uploadverzoek

Een uploadverzoek is gewone tekst met ingebedde bestandsgegevens. De header Content-Type noemt de boundary, en in de inhoud scheidt deze reeks de afzonderlijke delen. Elk deel heeft eigen kopregels met veldnaam en eventueel bestandsnaam. Een sterk ingekort, onschuldig voorbeeld:

POST /bewerbung HTTP/1.1
Content-Type: multipart/form-data; boundary=----trenner42

------trenner42
Content-Disposition: form-data; name="vorname"

Erika
------trenner42
Content-Disposition: form-data; name="lebenslauf"; filename="lebenslauf.pdf"
Content-Type: application/pdf

%PDF-1.7 ... Dateiinhalt ...
------trenner42--

Alle gegevens in deze tekst komen van de afzender, ook de bestandsnaam en de Content-Type van het deel. PHP neemt ze over in $_FILES, maar controleert ze niet op plausibiliteit.

De $_FILES-array

Voor een veld met de naam lebenslauf bevat $_FILES['lebenslauf'] volgens de PHP-handleiding deze sleutels:

Sleutel Inhoud Betrouwbaar?
name oorspronkelijke bestandsnaam bij de afzender nee
full_path door de browser gemeld pad, sinds PHP 8.1.0 nee
type MIME-type volgens de browser, door PHP niet gecontroleerd nee
size grootte in bytes door PHP bepaald
tmp_name pad van het tijdelijke bestand op de server door PHP toegekend
error foutcode van de uploadverwerking door PHP gezet

Bij velden met vierkante haken in de naam, bijvoorbeeld bilder[], bevat elke sleutel een array met één item per bestand. De toepassing doorloopt dan bijvoorbeeld $_FILES['bilder']['error'] en benadert via dezelfde index tmp_name en name.

Een formulier kan bovendien een verborgen veld MAX_FILE_SIZE vóór het bestandsveld verzenden. Volgens de handleiding is dat slechts een aanwijzing voor de browser, die gemakkelijk te omzeilen is. Betrouwbaar zijn uitsluitend de grenzen die de server afdwingt: in PHP, in de webserver en in de toepassing zelf.

Limieten: de php.ini-directives voor uploads

Deze directives sturen de uploadverwerking aan. De standaardwaarden komen uit de PHP-handleiding en gelden als geen php.ini ze overschrijft.

Directive Standaard Wijzigbaar in
file_uploads 1 alleen systeemconfiguratie
upload_tmp_dir leeg (tijdelijke map van het systeem) alleen systeemconfiguratie
upload_max_filesize 2M php.ini, pool, mapconfiguratie
post_max_size 8M php.ini, pool, mapconfiguratie
max_file_uploads 20 php.ini, pool, mapconfiguratie
enable_post_data_reading 1 php.ini, pool, mapconfiguratie

post_max_size moet groter zijn dan upload_max_filesize, en memory_limit hoort volgens de handleiding doorgaans groter te zijn dan post_max_size. Is upload_tmp_dir niet beschrijfbaar, dan valt PHP terug op de tijdelijke map van het systeem. Met open_basedir moet de tijdelijke map zijn toegestaan, anders mislukken uploads. Praktische waarden en hun gevolgen beschrijft het artikel post_max_size en upload_max_filesize zinvol begrenzen.

Foutcodes goed beoordelen

Het veld error geeft aan wat er bij de verwerking is gebeurd. De constanten lopen van UPLOAD_ERR_OK (0) via UPLOAD_ERR_INI_SIZE (1), UPLOAD_ERR_FORM_SIZE (2), UPLOAD_ERR_PARTIAL (3) en UPLOAD_ERR_NO_FILE (4) tot UPLOAD_ERR_NO_TMP_DIR (6), UPLOAD_ERR_CANT_WRITE (7) en UPLOAD_ERR_EXTENSION (8).

<?php
declare(strict_types=1);

$datei = $_FILES['lebenslauf'] ?? null;
if ($datei === null) {
    exit('Keine Upload-Daten, eventuell wurde post_max_size überschritten.');
}

$meldung = match ($datei['error']) {
    UPLOAD_ERR_OK => null,
    UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'Datei zu groß.',
    UPLOAD_ERR_NO_FILE => 'Keine Datei gewählt.',
    default => 'Upload fehlgeschlagen.',
};

if ($meldung === null && is_uploaded_file($datei['tmp_name'])) {
    $ziel = '/srv/app/uploads/' . bin2hex(random_bytes(16)) . '.pdf';
    move_uploaded_file($datei['tmp_name'], $ziel);
}

Het voorbeeld toont alleen de werking. Controle van het bestandstype, opslag buiten de webroot en het blokkeren van scriptuitvoering beschrijft de gids Bestandsuploads in PHP veilig realiseren.

Uploads en PHP2Shell: wat de overgangsmaatregelen betekenen

De PHP2Shell-lekken CVE-2026-109618 en CVE-2026-109619 betreffen de verwerking rond het verzoek in de PHP-interpreter. Daarom noemt php.net als overgangsmaatregelen onder meer post_max_size = 8M (zonder uploads) of 32M (met uploads), upload_max_filesize daaronder, en session.upload_progress.enabled=0. Deze waarden horen in php.ini, de FPM-pool of de serverconfiguratie; daarna moeten alle workers opnieuw starten.

Vakauteurs stellen bovendien voor multipart/form-data-verzoeken buiten echte uploadpaden te blokkeren op de proxy of in de WAF. Dat is een onofficiële maatregel met het risico dat formulieren niet meer werken; de afweging beschrijft het artikel Uploadverzoeken aan de rand filteren. Geen van deze maatregelen vervangt de update naar PHP 8.5.12, 8.4.27, 8.3.36 of 8.2.35.

Hoe helpt PHP SOS concreet?

  • Wij stellen vast welke van uw websites daadwerkelijk uploads nodig hebben en op welke paden.
  • Wij stellen de limieten en overgangsmaatregelen per pool in, starten alle workers opnieuw en testen de formulieren.
  • Wij controleren uploadmappen op uitvoerbare bestanden en blokkeren de scriptuitvoering in het kader van de serverhardening.
  • Wij voeren de PHP-update door en documenteren de patchstatus per website.

Veelgestelde vragen

Waar slaat PHP geüploade bestanden eerst op?

In een tijdelijke map. Standaard is dat de tijdelijke map van het systeem; met upload_tmp_dir kunt u die wijzigen. Wordt het bestand in het script niet verplaatst, dan verwijdert PHP het aan het einde van het verzoek.

Mag ik het veld type in $_FILES vertrouwen?

Nee. Het MIME-type wordt door de browser gemeld; PHP controleert het volgens de handleiding niet. Ook name en full_path komen van de afzender. Het werkelijke bestandstype bepaalt u aan de serverkant aan de hand van de inhoud.

Waarom is $_FILES leeg, terwijl er een bestand is verzonden?

Meestal was het hele verzoek groter dan post_max_size. Dan maakt PHP $_POST en $_FILES volledig leeg, zonder foutcode in de array. Andere oorzaken zijn een ontbrekende enctype in het formulier of uitgeschakelde file_uploads.

Helpt het tegen PHP2Shell om uploads in de toepassing te verbieden?

Nee. De verwerking van de verzoekgegevens gebeurt in PHP, voordat de toepassingscode draait. Effectief zijn de PHP-update en de herstart van alle workers; strengere limieten zijn slechts een overgangsmaatregel.

Wat is het verschil tussen upload_max_filesize en post_max_size?

upload_max_filesize begrenst elk afzonderlijk bestand, post_max_size de volledige inhoud van het verzoek inclusief alle bestanden en velden. post_max_size moet daarom groter zijn dan upload_max_filesize.

Bronnen

Direct contact

Elk uur telt. Schrijf ons nu.

Beschrijf kort het systeem, de PHP-versie en wat u opmerkt. Wij nemen contact op, brengen de situatie in kaart en werken op afstand tot het incident is afgerond en gedocumenteerd.

Directe hulp