Stručne: Keď prehliadač nahráva súbor, odošle požiadavku POST vo formáte multipart/form-data, v ktorej polia formulára a obsah súborov nasledujú za sebou ako jednotlivé časti. PHP túto požiadavku rozloží samo, každú časť so súborom zapíše do dočasného súboru v upload_tmp_dir, údaje uloží do $_FILES a až potom spustí váš skript. Skript súbor presunie pomocou move_uploaded_file(); všetko, čo sa nepresunie, PHP na konci požiadavky zmaže.
Cesta súboru od prehliadača až do skriptu
Pre rozhodovateľov je dôležité predovšetkým jedno: nahrávanie nie je úkon samotnej aplikácie. Veľká časť práce prebieha v interpreteri PHP skôr, než sa kód CMS alebo obchodu vôbec zapojí. Preto chyby v tejto časti PHP postihujú každý web, či už má formulár na nahrávanie, alebo nie.
Priebeh v prehľade:
- Odošle sa formulár s
method="POST"aenctype="multipart/form-data". - Webový server prijme požiadavku a odovzdá ju PHP, podľa spôsobu prevádzky cez PHP-FPM, mod_php alebo iné rozhranie.
- PHP skontroluje limity, napríklad
post_max_size, a rozloží obsah podľa oddeľovacieho reťazca, takzvanej boundary. - Bežné polia skončia v
$_POST, časti so súbormi sa zapíšu do dočasných súborov a opíšu sa v$_FILES. - Až teraz sa spustí skript, súbor skontroluje a presunie na cieľové miesto.
- Na konci požiadavky PHP zmaže všetky dočasné súbory, ktoré sa nepresunuli.
multipart/form-data: štruktúra požiadavky na nahrávanie
Požiadavka na nahrávanie je čistý text s vloženými údajmi súboru. Hlavička Content-Type uvádza boundary a v obsahu tento reťazec oddeľuje jednotlivé časti. Každá časť má vlastné hlavičky s názvom poľa a prípadne názvom súboru. Veľmi skrátený, neškodný prí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šetky údaje v tomto texte pochádzajú od odosielateľa, vrátane názvu súboru a Content-Type danej časti. PHP ich prevezme do $_FILES, ale nekontroluje ich vierohodnosť.
Pole $_FILES
Pre pole s názvom lebenslauf obsahuje $_FILES['lebenslauf'] podľa príručky PHP tieto kľúče:
| Kľúč | Obsah | Dôveryhodné? |
|---|---|---|
name |
pôvodný názov súboru u odosielateľa | nie |
full_path |
cesta nahlásená prehliadačom, od PHP 8.1.0 | nie |
type |
typ MIME podľa prehliadača, PHP ho nekontroluje | nie |
size |
veľkosť v bajtoch | zisťuje PHP |
tmp_name |
cesta k dočasnému súboru na serveri | prideľuje PHP |
error |
chybový kód spracovania nahrávania | nastavuje PHP |
Pri poliach s hranatými zátvorkami v názve, napríklad bilder[], obsahuje každý kľúč pole s jedným záznamom na súbor. Aplikácia potom prechádza napríklad $_FILES['bilder']['error'] a cez ten istý index pristupuje k tmp_name a name.
Formulár môže pred poľom pre súbor navyše odoslať skryté pole MAX_FILE_SIZE. Podľa príručky je to iba pokyn pre prehliadač, ktorý sa dá ľahko obísť. Spoľahnúť sa môžete výlučne na hranice, ktoré presadzuje server: v PHP, vo webovom serveri a v samotnej aplikácii.
Limity: direktívy php.ini pre nahrávanie
Tieto direktívy riadia spracovanie nahrávania. Štandardné hodnoty pochádzajú z príručky PHP a platia, ak ich nepredefinuje žiadny súbor php.ini.
| Direktíva | Štandard | Zmeniteľná v |
|---|---|---|
file_uploads |
1 |
iba systémová konfigurácia |
upload_tmp_dir |
prázdna (systémový temp) | iba systémová konfigurácia |
upload_max_filesize |
2M |
php.ini, pool, konfigurácia adresára |
post_max_size |
8M |
php.ini, pool, konfigurácia adresára |
max_file_uploads |
20 |
php.ini, pool, konfigurácia adresára |
enable_post_data_reading |
1 |
php.ini, pool, konfigurácia adresára |
post_max_size musí byť väčšie než upload_max_filesize a memory_limit by podľa príručky malo byť spravidla väčšie než post_max_size. Ak do upload_tmp_dir nemožno zapisovať, PHP prejde na temp adresár systému. S open_basedir musí byť temp adresár povolený, inak nahrávanie zlyhá. Praktické hodnoty a ich dôsledky opisuje článok Rozumné obmedzenie post_max_size a upload_max_filesize.
Správne vyhodnotenie chybových kódov
Pole error hovorí, čo sa pri spracovaní stalo. Konštanty siahajú od UPLOAD_ERR_OK (0) cez 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);
}
Príklad ukazuje iba mechaniku. Kontrolu typu súboru, uloženie mimo webrootu a zablokovanie spúšťania skriptov opisuje sprievodca Bezpečná implementácia nahrávania súborov v PHP.
Nahrávanie a PHP2Shell: čo znamenajú prechodné opatrenia
Zraniteľnosti PHP2Shell CVE-2026-109618 a CVE-2026-109619 sa týkajú spracovania v okolí požiadavky v interpreteri PHP. Preto php.net ako prechodné opatrenia uvádza okrem iného post_max_size = 8M (bez nahrávania) alebo 32M (s nahrávaním), upload_max_filesize pod touto hodnotou a session.upload_progress.enabled=0. Tieto hodnoty patria do php.ini, poolu FPM alebo konfigurácie servera, potom sa musia reštartovať všetky workery.
Odborní autori navyše navrhujú blokovať požiadavky multipart/form-data mimo skutočných ciest na nahrávanie na proxy alebo vo WAF. Ide o neoficiálne opatrenie s rizikom, že formuláre prestanú fungovať; zvažovanie opisuje článok Filtrovanie požiadaviek na nahrávanie na okraji. Žiadne z týchto opatrení nenahrádza aktualizáciu na PHP 8.5.12, 8.4.27, 8.3.36 alebo 8.2.35.
Ako PHP SOS konkrétne pomáha?
- Zisťujeme, ktoré z vašich webov nahrávanie skutočne potrebujú a na akých cestách.
- Nastavujeme limity a prechodné opatrenia pre každý pool, reštartujeme všetky workery a testujeme formuláre.
- Kontrolujeme adresáre na nahrávanie, či neobsahujú spustiteľné súbory, a blokujeme spúšťanie skriptov v rámci zabezpečenia servera.
- Nasadzujeme aktualizáciu PHP a dokumentujeme stav záplat pre každý web.
Časté otázky
Kam PHP ukladá nahrané súbory ako prvé?
Do dočasného adresára. Štandardne je to dočasný adresár systému; pomocou upload_tmp_dir sa dá zmeniť. Ak sa súbor v skripte nepresunie, PHP ho na konci požiadavky zmaže.
Môžem veriť poľu type v $_FILES?
Nie. Typ MIME hlási prehliadač a PHP ho podľa príručky nekontroluje. Aj name a full_path pochádzajú od odosielateľa. Skutočný typ súboru zistíte na strane servera podľa obsahu.
Prečo je $_FILES prázdne, hoci bol súbor odoslaný?
Zvyčajne bola celá požiadavka väčšia než post_max_size. PHP potom úplne vyprázdni $_POST aj $_FILES, bez chybového kódu v poli. Ďalšie príčiny sú chýbajúci enctype vo formulári alebo vypnuté file_uploads.
Pomôže proti PHP2Shell, ak v aplikácii zakážem nahrávanie?
Nie. Spracovanie údajov požiadavky prebieha v PHP skôr, než sa spustí kód aplikácie. Účinná je aktualizácia PHP a reštart všetkých workerov; prísnejšie limity sú iba prechodné opatrenie.
Aký je rozdiel medzi upload_max_filesize a post_max_size?
upload_max_filesize obmedzuje každý jednotlivý súbor, post_max_size celý obsah požiadavky vrátane všetkých súborov a polí. post_max_size preto musí byť väčšie než upload_max_filesize.