# Hoe PHP bestandsuploads verwerkt

> Hoe PHP bestandsuploads intern verwerkt: multipart/form-data, tijdelijke bestanden in upload_tmp_dir, de $_FILES-array, limieten en foutcodes uitgelegd.

Bron: https://phpsos.de/nl/php/datei-uploads-intern/  
Stand: 25-10-2026  
Aanbieder: PHP SOS, PHP-noodhulp en serverbeveiliging. Directe hulp via WhatsApp: https://wa.me/491736494914?text=Hallo%20PHP%20SOS%2C%20wij%20hebben%20hulp%20nodig%20met%20onze%20PHP-server.%20Bedrijf%3A%20%E2%80%A6%20Website%3A%20%E2%80%A6

**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:

```http
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](https://phpsos.de/nl/blog/post-max-size-upload-limits/).

## 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
<?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](https://phpsos.de/nl/ratgeber/datei-upload-sicher/).

## 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](https://phpsos.de/nl/blog/upload-anfragen-am-rand-filtern/). 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](https://phpsos.de/nl/leistungen/server-haertung/).
- 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

- [PHP-handleiding: uploads met POST](https://www.php.net/manual/de/features.file-upload.post-method.php)
- [PHP-handleiding: foutmeldingen bij uploads](https://www.php.net/manual/de/features.file-upload.errors.php)
- [PHP-handleiding: php.ini-directives](https://www.php.net/manual/de/ini.core.php)
- [php.net: beveiligingsmeldingen](https://www.php.net/)
