Stručně: XXE v PHP zabráníte tak, že XML z cizích zdrojů načítáte bez voleb LIBXML_NOENT, LIBXML_DTDLOAD a LIBXML_DTDVALID, odmítáte dokumenty s vlastní DTD a nastavíte entity loader, který externí zdroje odepře. Od libxml 2.9.0 PHP entity ve výchozím stavu nenahrazuje, proto je libxml_disable_entity_loader() od PHP 8.0 zastaralá. Od PHP 8.4 s libxml 2.13 je navíc k dispozici LIBXML_NO_XXE.
Stará zranitelnost, která nemizí
XML External Entities, zkráceně XXE, považuje mnoho vývojářů za vyřešené téma. PHP s moderními verzemi libxml načítání externích entit ve výchozím stavu vypnulo a stará ochranná funkce byla s PHP 8.0 označena jako zastaralá. Přesto se zranitelnosti XXE při kontrolách dál objevují.
Důvod je téměř vždy jedna ze tří situací: někdo nastavil volbu k vyřešení problému s importem a tím znovu zapnul nahrazování entit. Knihovna to dělá interně. Nebo aplikace běží na starém systému, jehož PHP bylo sestaveno s velmi starou libxml.
XXE je nebezpečné, protože parser XML přitom dotahuje obsah, který je v dokumentu jen odkazován: lokální soubory nebo adresy v síti. Možnými důsledky jsou čtení konfiguračních souborů, požadavky na interní systémy podobně jako u SSRF a přetížení serveru.
Co platí od libxml 2.9 a PHP 8.0
Příručka PHP popisuje situaci takto: od libxml 2.9.0 je nahrazování entit ve výchozím stavu vypnuto. Externí entity proto není nutné zvlášť vypínat, pokud se interní odkazy na entity nemají rozvíjet určitými volbami. Z toho vyplývá tento přehled:
| Volba | Účinek | Riziko u cizího XML |
|---|---|---|
LIBXML_NOENT |
nahrazuje entity | podle příručky může útoky XXE usnadnit |
LIBXML_DTDLOAD |
načítá externí podmnožinu | aktivuje načítání externích entit |
LIBXML_DTDVALID |
validuje proti DTD | může útoky XXE usnadnit |
LIBXML_NONET |
zakazuje síťové přístupy při načítání | ochranná doplňková volba |
LIBXML_NO_XXE |
vypíná externí entity při nahrazování | ochranná, od PHP 8.4 a libxml 2.13.0 |
LIBXML_PARSEHUGE |
ruší vnitřní limity parseru | usnadňuje přetížení, jen pro důvěryhodná data |
Zavádějící název LIBXML_NOENT je známá past: zní jako „žádné entity“, ale způsobuje opak.
Pro vývojáře z toho plyne: riziková není sama o sobě načítání XML, ale každé místo, které výchozí chování vědomě mění. Hledání těchto konstant v kódu je proto nejrychlejší cesta k první orientaci, ještě před jakoukoli náročnou kontrolou.
Bezpečná obrana proti XXE v PHP: vzor pro DOMDocument
Následující příklad kombinuje několik ochranných vrstev. Nastaví entity loader, který odepře každý externí zdroj, načítá bez rizikových voleb a odmítá dokumenty s vlastní DTD:
<?php
declare(strict_types=1);
function ladeFremdesXml(string $xml): DOMDocument
{
// Externí zdroje zásadně odepřít
libxml_set_external_entity_loader(
static fn (?string $public, string $system, array $context) => null
);
$optionen = LIBXML_NONET;
if (defined('LIBXML_NO_XXE')) {
$optionen |= LIBXML_NO_XXE;
}
$vorher = libxml_use_internal_errors(true);
$dom = new DOMDocument();
$ok = $dom->loadXML($xml, $optionen);
libxml_use_internal_errors($vorher);
if (!$ok) {
throw new InvalidArgumentException('XML ungültig.');
}
if ($dom->doctype !== null) {
throw new InvalidArgumentException('Dokumente mit DTD werden nicht angenommen.');
}
return $dom;
}
Podle příručky PHP blokuje návratová hodnota null v loaderu načtení externího zdroje. OWASP upozorňuje, že nestačí předat null přímo funkci libxml_set_external_entity_loader(); je třeba funkce, která null vrací. Pro SimpleXML a XMLReader platí stejné volby. Kdekoli je to možné, OWASP doporučuje DTD odmítat úplně.
Pokud můžete formát dat určit sami, je JSON pro výměnu mezi vlastními systémy zpravidla jednodušší volbou. U cizích formátů jako SAML nebo soubory Office zůstává XML nevyhnutelné.
Nalezení skrytých vstupů XML
Mnoho aplikací zpracovává víc XML, než si jejich vývojáři myslí. Projděte tento seznam:
- Rozhraní: klienti a servery SOAP, XML-RPC, platební a dopravní služby s odpověďmi v XML.
- Přihlášení: single sign-on přes SAML je založen na dokumentech XML, které přicházejí zvenčí.
- Importy: data produktů, kanály, mapy webu, formáty katalogů.
- Uploady: obrázky SVG jsou XML; formáty Office jako DOCX a XLSX se interně skládají ze souborů XML.
- Knihovny: balíčky Composeru pro dokumenty, tabulky nebo SAML, které interně používají libxml.
V kódu pomohou dvě vyhledávání:
grep -rnE "LIBXML_(NOENT|DTDLOAD|DTDVALID|PARSEHUGE)" src/ vendor/ --include="*.php"
grep -rnE "(loadXML|simplexml_load_(string|file)|XMLReader|->load\()" src/ --include="*.php"
Nálezy ve vendor/ nejsou důvodem k panice, ale podnětem ke kontrole verze a bezpečnostních hlášení knihovny.
U nahrávání SVG je XXE jen jedním z několika rizik, protože soubory SVG mohou obsahovat i skripty. Pokud váš program formát nutně nepotřebuje, nepovolujte ho pro uploady vůbec. Jinak SVG zpracovávejte jen výše uvedeným bezpečným vzorem a soubory poskytujte tak, aby prohlížeč obsažené skripty nespouštěl.
Věnujte pozornost také chybovým hlášením. Pokud aplikace vrací uživatelům hlášení libxml nefiltrovaně, mohou v nich být části zpracovávaného obsahu. Chyby patří do logu, uživatel dostane obecné hlášení; ve výše uvedeném příkladu zajišťuje libxml_use_internal_errors(true), že se varování nedostanou do výstupu. Omezte navíc velikost přijatých dokumentů XML dříve, než je parser vůbec uvidí.
Starší systémy: kdy výchozí ochrana chybí
Ochrana výchozím chováním závisí na verzi libxml, s níž bylo PHP sestaveno. Jaká verze běží, ukáže php -r 'echo LIBXML_DOTTED_VERSION, PHP_EOL;' na příkazové řádce nebo výstup phpinfo() v chráněném prostředí. Hodnotu ověřte pro každou verzi PHP na serveru, protože CLI a webserver mohou používat různé buildy. Image kontejnerů přinášejí vlastní libxml; tam kontrolujte v běžícím kontejneru, ne na hostiteli.
Obzvlášť zasaženy jsou systémy, které tak jako tak běží na starých verzích PHP. Pro PHP 8.1 a starší už po PHP2Shell neexistují oficiální aktualizace projektu PHP; co to znamená, popisuje stránka PHP 8.1 a starší: co platí nyní. Starý systém se zastaralou libxml a otevřenými zranitelnostmi interpretu by neměl být jen zabezpečen v kódu, ale zmigrován. Související rizika při zpracování cizích dat popisuje Nezabezpečená deserializace v PHP, stahování interních cílů Prevence Server-Side Request Forgery v PHP.
Jak konkrétně pomáhá PHP SOS?
- Najdeme všechna místa, kde vaše aplikace a její knihovny zpracovávají XML z cizích zdrojů.
- Odstraníme riziková nastavení parseru a zavedeme centrální zabezpečenou funkci pro načítání.
- Zkontrolujeme verze libxml a PHP na všech serverech a u starších systémů naplánujeme migraci.
- Změny otestujeme neškodnými kontrolními dokumenty a výsledek zdokumentujeme.
Kontrolu vlastního softwaru popisuje služba Bezpečnostní audit a penetrační test.
Časté dotazy
Jsou aplikace PHP od PHP 8 před XXE chráněny automaticky?
Ve výchozím stavu převážně ano, protože libxml od verze 2.9.0 entity už nenahrazuje. Ochrana však odpadá, jakmile kód nastaví volby jako LIBXML_NOENT, LIBXML_DTDLOAD nebo LIBXML_DTDVALID, nebo je PHP sestaveno s velmi starou verzí libxml.
Mám ještě volat libxml_disable_entity_loader?
Ne. Funkce je od PHP 8.0 zastaralá, protože načítání externích entit je ve výchozím stavu vypnuto. Pro další ochranu doporučuje příručka PHP libxml_set_external_entity_loader a od PHP 8.4 s vhodnou libxml konstantu LIBXML_NO_XXE.
Kde moje aplikace vůbec zpracovává XML?
Často na nečekaných místech: rozhraní SOAP, single sign-on přes SAML, kanály RSS, mapy webu, importy produktů, nahrané soubory SVG a soubory Office, které se interně skládají z XML. Také knihovny mohou zpracovávat XML, aniž by to bylo ve vlastním kódu vidět.
Co je útok Billion Laughs?
Dokument s vnořenými entitami, který se při rozvinutí enormně rozroste a spotřebuje paměť i výpočetní čas. Patří do stejné rodiny jako XXE. Ochranu poskytuje vypnutí nahrazování entit a odmítání dokumentů s vlastní DTD.