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:
- Het formulier met
method="POST"enenctype="multipart/form-data"wordt verzonden. - De webserver accepteert het verzoek en geeft het door aan PHP, afhankelijk van de werkwijze via PHP-FPM, mod_php of een andere interface.
- PHP controleert limieten zoals
post_max_sizeen splitst de inhoud op aan de hand van een scheidingsreeks, de zogeheten boundary. - Gewone velden komen in
$_POSTterecht, bestandsdelen worden naar tijdelijke bestanden geschreven en in$_FILESbeschreven. - Pas nu start het script, controleert het bestand en verplaatst het naar de bestemming.
- 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.