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:
- Odešle se formulář s
method="POST"aenctype="multipart/form-data". - 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í.
- PHP zkontroluje limity, například
post_max_size, a obsah rozdělí podle oddělovacího řetězce, takzvané boundary. - Běžná pole skončí v
$_POST, části se soubory se zapíšou do dočasných souborů a popíšou se v$_FILES. - Teprve nyní se spustí skript, soubor zkontroluje a přesune na cílové místo.
- 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:
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.
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
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.
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ě. Žá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 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.