Aktuální situace: PHP2Shell. Dvě kritické zranitelnosti PHP (CVSS 9.2) umožňují spustit škodlivý kód bez přihlášení. Záplaty od 21. 10. 2026.Co nyní udělat →

Jak PHP zpracovává nahrávání souborů

Mezi kliknutím na Nahrát a vaším kódem leží v PHP celý řetězec zpracování. Tato stránka vysvětluje multipart/form-data, dočasné soubory, $_FILES, limity a souvislost s PHP2Shell.

Stav 4 min čteníZnalosti o PHP

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:

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.

Zdroje

Okamžitý kontakt

Každá hodina se počítá. Napište nám hned.

Stručně popište systém, verzi PHP a pozorování. Ozveme se, vyjasníme situaci a budeme pracovat na dálku, dokud nebude incident uzavřen a zdokumentován.

Okamžitá pomoc