# Bezpečné nahrávání souborů v PHP: bezpečnost uploadu

> Bezpečnost uploadu v PHP v praxi: typ souboru kontrolujte na serveru, soubory přejmenujte, ukládejte mimo webroot a zakažte spouštění PHP.

Zdroj: https://phpsos.de/cs/ratgeber/datei-upload-sicher/  
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ě:** U bezpečnosti uploadu v PHP nerozhoduje jedna kontrola, ale jejich kombinace: typ souboru kontrolovat na serveru, soubory přejmenovat, ukládat mimo webroot a ve složce pro nahrávání nepovolit spouštění skriptů. Nezkontrolovaný formulář pro nahrávání je jednou z klasických cest, kterou útočníci dostanou zadní vrátka na webový server.

## Proč jsou formuláře pro nahrávání oblíbenou branou

Každý formulář, který přijímá soubory, zapisuje na váš server cizí data. Patří k nim formuláře pro uchazeče o práci, zákaznické portály, galerie obrázků, oblasti podpory a mnohá rozšíření CMS a e-shopů. Pokud útočníkovi uspěje uložit tam soubor, který webový server následně provede jako PHP, má trvalá zadní vrátka.

Pro vedení to znamená: nevinně vypadající upload životopisů může v nejhorším případě vést k převzetí celého webového serveru. Ve složkách pro nahrávání navíc často leží osobní údaje, které jsou při chybné konfiguraci veřejně dostupné.

Pro správce a vývojáře to znamená: kód aplikace a konfigurace serveru musí do sebe zapadat. Slabinu jedné úrovně často zachytí druhá, pokud jsou obě čistě provedeny. Jak najít již podstrčené soubory, popisuje příručka [Jak poznat a odstranit webshell](https://phpsos.de/cs/ratgeber/webshell-erkennen/).

## Základní pravidla bezpečnosti uploadu v PHP

Následující pravidla odpovídají doporučením OWASP Cheat Sheets a v provozu se osvědčila:

- **Seznam povolených místo seznamu zakázaných:** Přijímat jen výslovně povolené typy souborů, například PDF, JPG a PNG. Seznamy zakázaných přípon lze obejít variantami a dvojitými příponami.
- **Kontrolovat obsah, nejen příponu:** Skutečný typ zjistit na serveru pomocí `finfo`. Typ MIME v `$_FILES` je údaj odesílatele a není důvěryhodný.
- **Nový název souboru:** Původní název nikdy nepoužívat jako název souboru. Vygenerovat náhodný název a připojit příponu ze seznamu povolených.
- **Uložení mimo webroot:** Soubory ukládat tak, aby nebyly přímo dostupné přes URL. Doručuje se skriptem s kontrolou oprávnění.
- **Omezit velikost:** V aplikaci, v PHP i ve webovém serveru.
- **Žádné spouštění:** Pokud uploady přece musí ležet ve webrootu, vypnout v tomto adresáři spouštění skriptů.
- **Kontrola jen na serveru:** Kontroly v JavaScriptu v prohlížeči zlepšují obsluhu, ale nechrání.

## Bezpečný příklad s finfo a náhodným názvem

Následující příklad ukazuje jádro bezpečného zpracování. Je záměrně stručný a je nutné ho zasadit do vaší aplikace, tedy s přihlášením, kontrolou oprávnění, ochranou CSRF a protokolováním.

```php
<?php
declare(strict_types=1);

const MAX_BYTES  = 5 * 1024 * 1024; // 5 MB
const ERLAUBT    = [
    'application/pdf' => 'pdf',
    'image/jpeg'      => 'jpg',
    'image/png'       => 'png',
];
const ZIELORDNER = '/var/www/daten/uploads'; // mimo webroot

$datei = $_FILES['dokument'] ?? null;

if (!is_array($datei) || $datei['error'] !== UPLOAD_ERR_OK) {
    http_response_code(400);
    exit('Upload fehlgeschlagen.');
}

if ($datei['size'] > MAX_BYTES || !is_uploaded_file($datei['tmp_name'])) {
    http_response_code(400);
    exit('Datei ungültig.');
}

$finfo = new finfo(FILEINFO_MIME_TYPE);
$typ   = $finfo->file($datei['tmp_name']);

if (!isset(ERLAUBT[$typ])) {
    http_response_code(415);
    exit('Dateityp nicht erlaubt.');
}

$name = bin2hex(random_bytes(16)) . '.' . ERLAUBT[$typ];
$ziel = ZIELORDNER . '/' . $name;

if (!move_uploaded_file($datei['tmp_name'], $ziel)) {
    http_response_code(500);
    exit('Speichern fehlgeschlagen.');
}

chmod($ziel, 0640);
// Původní název ukládat do databáze jen jako text
// a při každém výstupu escapovat pomocí htmlspecialchars().
```

U obrázků se vyplatí další krok: obrázek znovu zakódovat pomocí GD nebo Imagick a přitom zahodit metadata. To ostatní kontroly nenahrazuje, ale snižuje zbytkové riziko způsobené vloženými cizími daty.

U dokumentů z neznámých zdrojů, například přihlášek nebo fotografií k reklamacím, je navíc před předáním užitečná antivirová kontrola. Chrání méně webový server než zaměstnance, kteří soubory později otevírají na svých pracovních počítačích. Každý upload také protokolujte s časem, účtem, velikostí souboru a rozpoznaným typem, aby šlo neobvyklé vzory později dohledat.

U CMS a e-shopů mají zabudované knihovny médií většinou vlastní kontroly. Rizikovější jsou rozšíření formulářů, funkce importu a vlastní programované doplňkové stránky, které ukládají soubory mimo knihovnu. Právě tato místa byste měli při kontrole prověřit jako první.

> **Tip:**
> Soubory z chráněné složky doručujte s `Content-Disposition: attachment` a `X-Content-Type-Options: nosniff`. Prohlížeč pak problematický obsah nezobrazí přímo, ale nabídne ho ke stažení.

## Vypnutí spouštění PHP ve složce pro nahrávání

I když soubor se špatným obsahem proklouzne, webový server ho nesmí spustit jako skript. Tato druhá ochranná vrstva patří na každý server, který ukládá uploady do webrootu.

Pro Nginx s PHP-FPM:

```nginx
location ^~ /uploads/ {
    location ~* \.(php|phtml|phar)$ {
        deny all;
    }
}
```

Pro Apache, nejlépe přímo ve VirtualHost místo v `.htaccess`, aby pravidlo nemohla aplikace přepsat:

```apache
<Directory "/var/www/html/uploads">
    AllowOverride None
    <FilesMatch "\.(php|phtml|phar)$">
        Require all denied
    </FilesMatch>
</Directory>
```

Pokud PHP běží jako modul Apache (mod_php), můžete ve stejném bloku navíc nastavit `php_admin_flag engine off`. Po změně ověřte neškodným testovacím souborem, který jen vypíše text, že server odpoví na požadavek na něj kódem 403.

## Limity velikosti a souvislost s PHP2Shell

Uploady úzce souvisejí s nastavením PHP `post_max_size` a `upload_max_filesize`. V souvislosti s PHP2Shell doporučuje php.net jako přechodné opatření nastavit `post_max_size` na 8M, pokud nejsou uploady potřeba, nebo na 32M s uploady, a `upload_max_filesize` držet pod ním. Není to úplná ochrana a nenahrazuje to aktualizaci na opravenou verzi PHP.

```ini
; php.ini nebo pool FPM (php_admin_value), poté restartovat všechny workery
file_uploads        = On
post_max_size       = 32M
upload_max_filesize = 20M
max_file_uploads    = 5
```

Pokud web uploady vůbec nepotřebuje, je `file_uploads = Off` nejčistším řešením. Jaká nastavení mají smysl do záplaty, najdete v článku [Okamžitá opatření bez záplaty](https://phpsos.de/cs/php2shell/sofortmassnahmen-ohne-patch/). Vhodná práva pro adresáře uploadů popisuje příručka [Práva k souborům na webovém serveru](https://phpsos.de/cs/ratgeber/dateirechte-webserver/).

## Typické chyby ve funkcích pro nahrávání

Při procházení stávajících aplikací se pořád objevují stejné vzory. Zkontrolujte své formuláře cíleně na tyto body:

- Přípona se kontroluje jen částečně, takže názvy souborů s více příponami proklouznou.
- Původní název z formuláře skončí v cestě k uložení. To otevírá cesty k manipulaci s cestami a k přepsání existujících souborů.
- Uploady leží veřejně s předvídatelnými názvy. U přihlášek nebo smluv je to i problém ochrany osobních údajů.
- Složka pro nahrávání je pro webový server zapisovatelná a zároveň spustitelná.
- Staré doplňky pro nahrávání nebo pluginy formulářů se neaktualizují, přestože výrobce vydal bezpečnostní aktualizace.
- Chyby se neprotokolují. Odmítnuté uploady jsou přitom časným signálem cílených pokusů.

## Jak konkrétně pomáhá PHP SOS?

- Kontrolujeme vaše funkce pro nahrávání v kódu i v konfiguraci serveru a pojmenujeme konkrétní slabiny s prioritou.
- Zákaz spouštění ve složce pro nahrávání, vhodná práva a limity velikosti nastavujeme v rámci [zabezpečení serveru](https://phpsos.de/cs/leistungen/server-haertung/).
- Při podezření prohledáme adresáře uploadů na podstrčené skripty a vyčistíme je v rámci našeho [odstranění malwaru](https://phpsos.de/cs/leistungen/malware-entfernung/).
- Na přání necháme provedení nezávisle ověřit [penetračním testem](https://phpsos.de/cs/leistungen/sicherheitsaudit-penetrationstest/).


## Časté dotazy

**Stačí při uploadu kontrolovat příponu souboru?**  
Ne. Přípona i typ MIME hlášený prohlížečem pocházejí od odesílatele a lze je libovolně zvolit. Kontrolujte obsah na serveru, přidělte nový název souboru a zakažte spouštění skriptů v adresáři pro nahrávání.

**Kam by se měly ukládat nahrané soubory?**  
Nejlépe mimo veřejný webový adresář. Doručení pak zajišťuje skript, který předem ověří, zda smí uživatel soubor vidět. Soubory tak nejsou ani přímo dostupné, ani spustitelné.

**Jsou soubory SVG jako obrázky neškodné?**  
Ne automaticky. SVG je XML a může obsahovat aktivní obsah. Povolte SVG jen tehdy, když soubory vyčistíte nebo je doručujete výhradně ke stažení s vhodnou hlavičkou Content-Disposition.

**Souvisí velikost uploadu s PHP2Shell?**  
Ano, nepřímo. php.net doporučuje jako přechodné opatření nižší hodnoty post_max_size a upload_max_filesize. To snižuje riziko, ale nenahrazuje aktualizaci na opravenou verzi PHP a restart všech workerů.

## Zdroje

- [OWASP Cheat Sheet Series (File Upload)](https://cheatsheetseries.owasp.org/)
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
- [php.net: bezpečnostní oznámení k PHP2Shell](https://www.php.net/)
