Nette Http 3.4.0 přináší ochranu proti SSRF útokům

před 6 hodinami od David Grudl  

Avatar uživatele. Obrázek vytvořený MCP serverem. OG náhled v komentáři. Webhook callback. OAuth redirect. RSS feed import. Kdykoli vaše aplikace stahuje URL, kterou poslal uživatel (nebo AI asistent), je terčem Server-Side Request Forgery (SSRF), jedné z OWASP Top 10 zranitelností.

Nette/Http 3.4.0 přináší dvě nové třídy, UrlValidator a IPAddress, které tenhle problém řeší jedním řádkem.

Jak fungují útoky typu SSRF v cloudových prostředích

Klasický SSRF útok vypadá nenápadně:

https://169.254.169.254/latest/meta-data/iam/security-credentials/

Adresa 169.254.169.254 je cloud metadata endpoint, dostupný jen zevnitř AWS, Azure nebo GCP instance. Útočník se na ni z venku nedostane, ale váš server ano. Pokud aplikace tu URL slepě stáhne (třeba jako „obrázek profilu uživatele“), pošle si útočník zpátky aktivní IAM klíče.

Další oblíbené cíle: https://192.168.1.1/admin/ (interní router), http://localhost:6379/ (Redis bez auth), https://10.0.5.7/internal-api/ (cokoli ve vaší LAN).

Naivní kontrola „je to https://?“ nestačí. Musíte vědět, na jaké IP adresy URL skutečně míří, ne jak vypadá. To znamená přeložit název na IP adresy přes DNS, zkontrolovat je proti seznamu zakázaných rozsahů a ošetřit záludnosti jako IPv4-mapped IPv6 nebo DNS rebinding… rychle se z toho stane padesát řádků kódu, který každý projekt řeší trochu jinak, obvykle špatně.

S nástupem MCP serverů a AI integrací útočná plocha výrazně narostla. AI dostane URL od uživatele a předá ji nástroji ke stažení. Server URL stáhne. AI nemá intuici pro „tohle je interní endpoint“, takže bez téhle ochrany je z AI agenta ideální cíl útoku.

Jak ověřit platnost URL adres v PHP

use Nette\Http\UrlValidator;

if (!(new UrlValidator)->allows($userUrl)) {
	return; // unsafe URL
}
// teď je bezpečné fetchovat

Default je přísný: jen https, jen port 443, hostname musí resolvnout výhradně na veřejné IP adresy. Blokuje loopback, privátní rozsahy, link-local včetně cloud metadata, multicast a IANA-reserved.

Když potřebujete jiná pravidla, nastavíte si je v konstruktoru:

// Interní monitoring potřebuje LAN (http a libovolný port)
new UrlValidator(schemes: ['http', 'https'], ports: null, allowPrivateIps: true);

// OAuth jen na známé partnery
new UrlValidator(
	hostAllowlist: ['*.partner1.com', '*.partner2.com'],
);

Wildcard *.example.com matchuje subdomény libovolné hloubky. Apex doménu přidejte zvlášť.

Prevence útoků typu DNS rebinding v PHP

Mezi allows() a samotným fetchem je krátké okno, kdy útočník kontrolující DNS hosta přepne A záznam z veřejné na interní IP. Validace projde, ale request půjde jinam (DNS rebinding útok).

Pro plnou obranu existuje getResolvedIPs(). Funguje stejně jako allows(): pro nebezpečné URL vrátí [], pro bezpečné pole zvalidovaných adres. Ty pak předáte cURL přes CURLOPT_RESOLVE:

$ips = (new UrlValidator)->getResolvedIPs($url);
if (!$ips) {
	return; // unsafe
}

$ch = curl_init($url);
$host = parse_url($url, PHP_URL_HOST);
curl_setopt($ch, CURLOPT_RESOLVE, ["$host:443:" . implode(',', $ips)]);

cURL už nedělá nový DNS dotaz, spojení jde přesně na ty IPs, které prošly validací. Časové okno pro DNS rebinding se tím zavírá.

Ověřování adres IPv4 a IPv6 v PHP

Současně přichází IPAddress, neměnný objekt pro práci s IPv4 a IPv6 adresami:

use Nette\Http\IPAddress;

$ip = new IPAddress('169.254.169.254');
$ip->isLinkLocal();                // true (cloud metadata)
$ip->isInRange('192.168.0.0/16');  // false

Plná podpora IPv4-mapped IPv6: ::ffff:127.0.0.1 se vyhodnotí jako loopback. Naivní kontrola tenhle zápis jako loopback nerozpozná, takže jím útočník její ochranu snadno obejde.

Použitelné nezávisle na UrlValidator všude, kde pracujete s IP: rate limiting, audit logy, trusted proxy validace. Statická IPAddress::isValid() jako rychlý checker, IPAddress::tryFrom() jako factory vracející ?self bez výjimky.


Co tahle dvojice neřeší: HTTP redirecty (váš klient by je měl mít vypnuté, nebo revalidovat každý hop) a útoky mířící na samotný obsah stažené odpovědi. Validace adresy není validace obsahu.

Co dalšího přináší 3.4

SSRF ochrana je vlajková loď, ale verze 3.4 toho přináší víc:

  • Konec CSRF tokenů: nová metoda Request::isFrom() pozná původ requestu z hlaviček Sec-Fetch-*, a je to tak velké téma, že jsme mu věnovali samostatný článek.
  • Modernější cookies: setCookie() nově posílá atribut Max-Age, podporuje partitioned cookies (CHIPS) parametrem partitioned: true a pro SameSite=None nebo partitioned cookie automaticky vynutí Secure.
  • Enum SameSite: místo stringových konstant IResponse::SameSiteLax a spol. nově píšete SameSite::Lax. Přijímá ho setCookie() i Session::setCookieParameters(), staré konstanty jsou deprecated.
  • Expirace všude stejně: setCookie(), Session::setExpiration() i expirace session sekcí vykládají hodnotu shodně: číslo je relativní počet sekund, text je interval ('20 minutes') nebo datum; setCookie() navíc přijímá DateTimeInterface. Předávání absolutního UNIX timestampu je deprecated. Session cookie představuje hodnota null, která nahradí dřívější 0.
  • detectLanguage() rozumí wildcardu: hlavička Accept-Language smí obsahovat * (posílají ho hlavně API klienti a boti). Dříve ho metoda ignorovala a vracela null, nově pro něj vrátí jeden z jazyků, které aplikace nabízí.
  • Knihovna nově vyžaduje PHP 8.3+, deprecated metoda Request::getRemoteHost() vrací null a dávno deprecated třída UserStorage byla odstraněna.

Vše výše popsané přichází s verzí nette/http 3.4.0.

David Grudl Tvůrce open-source projektů a specialista na AI, který lidem otevírá dveře do světa umělé inteligence. Jeho projekty Nette a další používají weby, které denně navštěvujete. Píše na Uměligence, La Trine a moderuje Tech Guys. Organizuje AI workshopy a věří, že technologie mají smysl jen tehdy, když lidem skutečně pomohou.