Přeskočit na obsah
Tech-Blog Chatujme.cz Chatujme.cz
Programování

Nette bralo adresu klienta ze začátku hlavičky Forwarded, kam si ji zapíše i útočník

Aplikace za reverzní proxy nevidí adresu návštěvníka přímo a musí ji vyčíst z hlavičky, kterou proxy k požadavku připojí. Nette Http do verze 3.4.0 bralo z hlavičky Forwarded první položku, jenže tu tam nezapsala proxy, nýbrž klient. Vydání 3.4.1 z 27. srpna 2026 čte řetěz od opačného konce a aplikacím nastaveným přes rozšíření DI hlavičku Forwarded ve výchozím stavu zakazuje.

· 69 zhlédnutí

Stránka vydání 3.4.1 knihovny Nette Http na GitHubu, pod nadpisem Breaking Changes stojí odrážka o hlavičkách X-Forwarded
Poznámky k vydání Nette Http 3.4.1 na GitHubu. První odrážka pod nadpisem Breaking Changes oznamuje, že aplikace nastavená přes rozšíření DI věří napříště jen hlavičkám X-Forwarded-*. Foto: vlastní snímek obrazovky, github.com (snímek obrazovky)

Web za reverzní proxy má v $_SERVER['REMOTE_ADDR'] adresu té proxy, ne návštěvníka. Skutečnou adresu klienta nese hlavička, kterou k požadavku připojí proxy cestou k aplikaci – a protože je to obyčejná hlavička, umí ji poslat i klient sám. Knihovna Nette Http ve verzi 3.4.1 z 27. srpna 2026 přepsala, jak se z ní adresa čte, a zpřísnila výchozí nastavení.

Proxy svůj údaj připisuje na konec

Standardní podobu té hlavičky popisuje RFC 7239 z června 2014. Proxy, která chce přidat svůj údaj, ho podle něj připojí za čárku k tomu, co v hlavičce už stojí. Vznikne řetěz zastávek a nejlevější položka je ta nejbližší klientovi:

Forwarded: for=203.0.113.7, for=192.0.2.1;proto=https;host=example.org

Kdo chce z takového řetězu vyčíst adresu skutečného návštěvníka, musí od pravého konce odečíst zastávky, které patří jeho vlastním proxy, a vzít první zbylou. Samo RFC k tomu dodává, že se na obsah hlavičky nedá spolehnout: změnit ji může každý uzel po cestě včetně klienta, který požadavek posílá.

Původní kód bral levý kraj řetězu

Nette do verze 3.4.0 dělalo pravý opak. Z hlavičky Forwarded vybíralo první hodnotu for – jenže tu tam nezapsala žádná proxy, dorazila od klienta. Kdo poslal vlastní hlavičku Forwarded a proxy ji nesmazala, podstrčil aplikaci libovolnou adresu i tehdy, když byla proxy nastavená správně.

Rozbor, který k opravě vznikl, uvádí vady čtyři. Druhá je v rozebírání hlavičky: kód dělil řetězec výrazem preg_split('/[,;]/'), takže oddělovač zastávek slil dohromady s oddělovačem parametrů uvnitř zastávky. Struktura se tím ztratila a údaje protohost nešlo přiřadit ke správné zastávce. Kód je proto bral, jen když byl v hlavičce právě jeden – a u každé poctivé vícezastávkové hlavičky o ně přišel.

Třetí vada: větev pro nestandardní X-Forwarded-For už tehdy uměla důvěryhodné proxy odečíst a vzít pravý kraj. Táž úloha tak měla v jedné knihovně dvě různá řešení, každé jinak nedodělané. A čtvrtá: RFC dovoluje místo adresy zapsat zástupný identifikátor for=unknown, který se z getRemoteAddress() vracel jako obyčejný řetězec, přestože ta metoda slibuje IP adresu.

Nette teď bere nejpravější zastávku, která už není jeho proxy

Přepsaný kód rozebírá hlavičku po zastávkách: nejdřív podle čárek na položky, pak podle středníků na parametry. Adresu klienta hledá společná metoda findClientHop(), kterou používají obě větve – odstraní z konce řetězu důvěryhodné proxy a vrátí nejpravější zbylou zastávku. Není-li v ní platná IP adresa, vrátí null; getRemoteAddress() tedy nově dává adresu, nebo nic.

Schéma a hostitele bere Nette ze stejné zastávky jako adresu, tedy z položky, kterou zapsala proxy obrácená ke klientovi. Podvrhnout je proto nejde a vícezastávková hlavička už nepotřebuje výjimku. Platí u ní k tomu pravidlo všechno, nebo nic: když se adresa klienta určit nepodaří, nepoužije se z hlavičky ani schéma, ani hostitel.

U rodiny X-Forwarded-* to takhle jít nemůže, protože tyhle hlavičky žádnou strukturu položek nemají. X-Forwarded-Host se čte na stejném indexu jako adresa klienta a bez ní se přeskočí, zato X-Forwarded-ProtoX-Forwarded-Port se použijí, kdykoli požadavek přišel přes důvěryhodnou proxy. Důvod je praktický: server, který jen ukončuje šifrované spojení, posílá právě jen X-Forwarded-Proto. Po hlavičkách rozdělenou důvěru zná i Symfony, kterému se v nastavení vyjmenuje, kterým hlavičkám od proxy věřit.

Výchozí nastavení už hlavičce Forwarded nevěří

Druhá půlka změny je konfigurace. Přibyla volba proxyHeaders se čtyřmi hodnotami – both, xForwarded, forwardednone – a rozšíření pro Nette DI ji nastavuje na xForwarded:

http:
	proxy: 10.0.0.0/8
	proxyHeaders: xForwarded   # výchozí, dál forwarded, both, none

Aplikace za proxy tedy klientem poslané hlavičce Forwarded nevěří, dokud si to provozovatel nevyžádá. Komu ji proxy opravdu nastavuje, ten volbu přepne na forwarded nebo both – je to jediná změna, kvůli které se knihovna nedá aktualizovat naslepo. Volání setProxy() z PHP zůstává na both, aby se nerozbil kód, který si knihovnu obsluhuje sám. Bez vyplněného seznamu proxy se ostatně nečte žádná z těch hlaviček vůbec.

Zbytek rizika se rozborem hlavičky odstranit nedá

Kterou hlavičku knihovna použije, rozhoduje jejich přítomnost: když dorazí obě a věří se oběma, vyhrává Forwarded. Z pouhé přítomnosti se ale nepozná, jestli hlavičku napsala proxy, nebo jestli ji poslal klient a proxy ji jen propustila dál. Proxy, která spravuje X-Forwarded-For a cizí Forwarded předává beze změny, tak aplikaci podstrčí podvrženou hodnotu i po opravě.

Autoři to v rozhodnutí přiznávají natvrdo. Rozborem hlavičky se to vyřešit nedá a týká se to stejně Symfony i Laravelu; proti tomu míří právě výchozí xForwarded, které propuštěnou hlavičku Forwarded ignoruje. Zbytek zůstává na správci proxy, která by měla zahodit každou hlavičku pro předávání, kterou sama nenastavuje.

Skriptům z příkazové řádky přibyla adresa

Druhá novinka téhož vydání s bezpečností nesouvisí. Skript spuštěný z příkazové řádky – cron, údržbová úloha – nemá v poli $_SERVER žádného hostitele, takže adresa požadavku nedávala smysl a všechno od ní odvozené taky ne. Nová volba http: baseUrl a metoda RequestFactory::setBaseUrl() takovému běhu adresu dodají, aby fungoval generátor odkazů i cesty ke statickým souborům v šablonách. Uplatní se jen tehdy, když se hostitele nedá zjistit odjinud, takže požadavků z webu se to netýká.

Ve stejné knihovně přibyl v červnovém vydání 3.4.0 i nástroj proti útokům, kdy aplikaci někdo přiměje stáhnout adresu podle svého: dvojice IPAddressUrlValidator umí adresu prověřit a vrátit i zjištěné IP adresy, aby se spojení dalo přišpendlit na ně. Nette Mail zase z podpisu DKIM vypustil údaj o délce těla, kvůli kterému šlo k podepsané zprávě připsat cizí text.

Zdroje: poznámky k vydání Nette Http 3.4.1, přehled vydání na nette.org, rozhodnutí o čtení hlaviček v repozitáři knihovny, RFC 7239, dokumentace Symfony k reverzním proxy.

Programování

PHP Nette Bezpečnost HTTP proxy

← zpět na výpis