# Jak PHP zpracovává nahrávání souborů

> Jak PHP zpracovává nahrávání souborů: multipart/form-data, dočasné soubory v upload_tmp_dir, pole $_FILES, limity a chybové kódy srozumitelně.

Zdroj: https://phpsos.de/cs/php/datei-uploads-intern/  
Stav: 25.10.2026  
Poskytovatel: PHP SOS, Nouzová pomoc pro PHP a bezpečnost serverů. Okamžitá pomoc přes WhatsApp: https://wa.me/491736494914?text=Dobr%C3%BD%20den%2C%20PHP%20SOS%2C%20pot%C5%99ebujeme%20pomoc%20s%20na%C5%A1%C3%ADm%20PHP%20serverem.%20Firma%3A%20%E2%80%A6%20Web%3A%20%E2%80%A6

**Stručně:** Když prohlížeč nahrává soubor, odešle požadavek POST ve formátu `multipart/form-data`, v němž za sebou následují pole formuláře a obsah souborů jako jednotlivé části. PHP tento požadavek rozloží samo, každou část se souborem zapíše do dočasného souboru v `upload_tmp_dir`, údaje uloží do `$_FILES` a teprve potom spustí váš skript. Skript soubor přesune pomocí `move_uploaded_file()`; vše, co se nepřesune, PHP na konci požadavku smaže.

## Cesta souboru od prohlížeče do skriptu

Pro rozhodovatele je důležité především jedno: nahrávání není záležitostí samotné aplikace. Velká část práce se odehrává v interpretu PHP, ještě než se kódu CMS nebo e-shopu vůbec někdo dotkne. Chyby v této části PHP proto postihují každý web, ať má formulář pro nahrávání, nebo ne.

Průběh v kostce:

1. Odešle se formulář s `method="POST"` a `enctype="multipart/form-data"`.
2. Webový server požadavek přijme a předá ho PHP, podle provozního režimu přes PHP-FPM, mod_php nebo jiné rozhraní.
3. PHP zkontroluje limity, například `post_max_size`, a obsah rozdělí podle oddělovacího řetězce, takzvané boundary.
4. Běžná pole skončí v `$_POST`, části se soubory se zapíšou do dočasných souborů a popíšou se v `$_FILES`.
5. Teprve nyní se spustí skript, soubor zkontroluje a přesune na cílové místo.
6. Na konci požadavku PHP smaže všechny dočasné soubory, které nebyly přesunuty.

## multipart/form-data: stavba požadavku s nahráváním

Požadavek s nahráváním je čistý text s vloženými daty souborů. Hlavička `Content-Type` uvádí boundary a v obsahu tento řetězec odděluje jednotlivé části. Každá část má vlastní hlavičky s názvem pole a případně názvem souboru. Silně zkrácený, neškodný příklad:

```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--
```

Všechny údaje v tomto textu pocházejí od odesílatele, včetně názvu souboru a `Content-Type` dané části. PHP je přebírá do `$_FILES`, ale nekontroluje jejich věrohodnost.

## Pole $_FILES

Pro pole s názvem `lebenslauf` obsahuje `$_FILES['lebenslauf']` podle příručky PHP tyto klíče:

| Klíč | Obsah | Důvěryhodné? |
|---|---|---|
| `name` | původní název souboru u odesílatele | ne |
| `full_path` | cesta hlášená prohlížečem, od PHP 8.1.0 | ne |
| `type` | typ MIME podle prohlížeče, PHP jej nekontroluje | ne |
| `size` | velikost v bajtech | zjistí PHP |
| `tmp_name` | cesta k dočasnému souboru na serveru | přidělí PHP |
| `error` | chybový kód zpracování nahrávání | nastaví PHP |

U polí s hranatými závorkami v názvu, například `bilder[]`, obsahuje každý klíč pole s jednou položkou na soubor. Aplikace pak prochází například `$_FILES['bilder']['error']` a přes stejný index přistupuje k `tmp_name` a `name`.

Formulář může navíc před polem pro soubor poslat skryté pole `MAX_FILE_SIZE`. Podle příručky jde jen o nápovědu pro prohlížeč, kterou lze snadno obejít. Spolehnout se můžete pouze na limity, které prosazuje server: v PHP, ve webovém serveru a v samotné aplikaci.

## Limity: direktivy php.ini pro nahrávání

Tyto direktivy řídí zpracování nahrávání. Výchozí hodnoty pocházejí z příručky PHP a platí, pokud je php.ini nepřepíše.

| Direktiva | Výchozí | Lze měnit v |
|---|---|---|
| `file_uploads` | `1` | jen v systémové konfiguraci |
| `upload_tmp_dir` | prázdné (systémový temp) | jen v systémové konfiguraci |
| `upload_max_filesize` | `2M` | php.ini, pool, konfigurace adresáře |
| `post_max_size` | `8M` | php.ini, pool, konfigurace adresáře |
| `max_file_uploads` | `20` | php.ini, pool, konfigurace adresáře |
| `enable_post_data_reading` | `1` | php.ini, pool, konfigurace adresáře |

`post_max_size` musí být větší než `upload_max_filesize` a `memory_limit` by měl být podle příručky zpravidla větší než `post_max_size`. Pokud do `upload_tmp_dir` nelze zapisovat, PHP přejde na temp adresář systému. S `open_basedir` musí být temp adresář povolen, jinak nahrávání selže. Praktické hodnoty a jejich důsledky popisuje článek [Rozumné omezení post_max_size a upload_max_filesize](https://phpsos.de/cs/blog/post-max-size-upload-limits/).

## Správné vyhodnocení chybových kódů

Pole `error` říká, co se při zpracování stalo. Konstanty sahají od `UPLOAD_ERR_OK` (0) přes `UPLOAD_ERR_INI_SIZE` (1), `UPLOAD_ERR_FORM_SIZE` (2), `UPLOAD_ERR_PARTIAL` (3) a `UPLOAD_ERR_NO_FILE` (4) až po `UPLOAD_ERR_NO_TMP_DIR` (6), `UPLOAD_ERR_CANT_WRITE` (7) a `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);
}
```

Příklad ukazuje jen mechaniku. Kontrolu typu souboru, uložení mimo webroot a zablokování spouštění skriptů popisuje průvodce [Bezpečné nahrávání souborů v PHP](https://phpsos.de/cs/ratgeber/datei-upload-sicher/).

## Nahrávání a PHP2Shell: co znamenají přechodná opatření

Zranitelnosti PHP2Shell CVE-2026-109618 a CVE-2026-109619 se týkají zpracování kolem požadavku v interpretu PHP. Proto php.net jako přechodná opatření uvádí mimo jiné `post_max_size = 8M` (bez nahrávání) nebo `32M` (s nahráváním), `upload_max_filesize` pod touto hodnotou a dále `session.upload_progress.enabled=0`. Tyto hodnoty patří do php.ini, poolu FPM nebo konfigurace serveru, poté musí všichni workeři naběhnout znovu.

Odborní autoři navíc navrhují blokovat požadavky `multipart/form-data` mimo skutečné cesty pro nahrávání na proxy nebo ve WAF. Jde o neoficiální opatření s rizikem, že přestanou fungovat formuláře; zvážení popisuje článek [Filtrování požadavků s nahráváním na okraji sítě](https://phpsos.de/cs/blog/upload-anfragen-am-rand-filtern/). Žádné z těchto opatření nenahrazuje aktualizaci na PHP 8.5.12, 8.4.27, 8.3.36 nebo 8.2.35.

## Jak konkrétně pomáhá PHP SOS?

- Zjistíme, které z vašich webů nahrávání skutečně potřebují a na jakých cestách.
- Nastavíme limity a přechodná opatření pro každý pool, restartujeme všechny workery a otestujeme formuláře.
- Zkontrolujeme adresáře pro nahrávání, zda neobsahují spustitelné soubory, a v rámci [zpevnění serveru](https://phpsos.de/cs/leistungen/server-haertung/) zablokujeme spouštění skriptů.
- Nasadíme aktualizaci PHP a zdokumentujeme stav záplat pro každý web.


## Časté dotazy

**Kam PHP nahrané soubory nejdříve uloží?**  
Do dočasného adresáře. Ve výchozím nastavení je to dočasný adresář systému; pomocí upload_tmp_dir ho lze změnit. Pokud skript soubor nepřesune, PHP jej na konci požadavku smaže.

**Mohu věřit poli type v $_FILES?**  
Ne. Typ MIME hlásí prohlížeč, PHP ho podle příručky nekontroluje. I name a full_path pocházejí od odesílatele. Skutečný typ souboru zjistíte na straně serveru podle obsahu.

**Proč je $_FILES prázdné, i když byl soubor odeslán?**  
Většinou byl celý požadavek větší než post_max_size. PHP pak $_POST i $_FILES zcela vyprázdní, a to bez chybového kódu v poli. Další příčinou může být chybějící enctype ve formuláři nebo vypnuté file_uploads.

**Pomůže proti PHP2Shell, když zakážu nahrávání v aplikaci?**  
Ne. Zpracování dat požadavku probíhá v PHP dříve, než se spustí kód aplikace. Účinná je aktualizace PHP a restart všech workerů; přísnější limity jsou jen přechodným opatřením.

**Jaký je rozdíl mezi upload_max_filesize a post_max_size?**  
upload_max_filesize omezuje každý jednotlivý soubor, post_max_size celý obsah požadavku včetně všech souborů a polí. post_max_size proto musí být větší než upload_max_filesize.

## Zdroje

- [Příručka PHP: Nahrávání metodou POST](https://www.php.net/manual/de/features.file-upload.post-method.php)
- [Příručka PHP: Chybová hlášení při nahrávání](https://www.php.net/manual/de/features.file-upload.errors.php)
- [Příručka PHP: Direktivy php.ini](https://www.php.net/manual/de/ini.core.php)
- [php.net: Bezpečnostní oznámení](https://www.php.net/)
