Ukratko: XXE u PHP-u sprječavate tako da XML iz stranih izvora učitavate bez opcija LIBXML_NOENT, LIBXML_DTDLOAD i LIBXML_DTDVALID, odbijate dokumente s vlastitim DTD-om i postavite Entity Loader koji odbija vanjske resurse. Od libxml-a 2.9.0 PHP zadano više ne zamjenjuje entitete, zato je libxml_disable_entity_loader() od PHP-a 8.0 zastarjela. Od PHP-a 8.4 s libxml-om 2.13 dostupna je dodatno i LIBXML_NO_XXE.
Stara ranjivost koja ne nestaje
XML External Entities, skraćeno XXE, mnogim se razvojnim inženjerima čine riješenom temom. PHP je s modernim verzijama libxml-a učitavanje vanjskih entiteta zadano isključio, a stara zaštitna funkcija označena je kao zastarjela s PHP-om 8.0. Unatoč tome XXE ranjivosti i dalje se pojavljuju u provjerama.
Razlog je gotovo uvijek u jednoj od tri situacije: netko je postavio opciju da riješi problem uvoza i time ponovno uključio zamjenu entiteta. Biblioteka to radi interno. Ili aplikacija radi na starom sustavu čiji je PHP izgrađen s vrlo starim libxml-om.
XXE je opasan jer XML parser pritom učitava sadržaje koji su u dokumentu samo referencirani: lokalne datoteke ili adrese u mreži. Moguće posljedice su čitanje konfiguracijskih datoteka, zahtjevi prema internim sustavima slično kao kod SSRF-a i preopterećenje poslužitelja.
Što vrijedi od libxml-a 2.9 i PHP-a 8.0
PHP priručnik stanje opisuje ovako: od libxml-a 2.9.0 zamjena entiteta zadano je deaktivirana. Vanjske entitete stoga više ne treba posebno isključivati, osim ako se unutarnje reference na entitete trebaju razriješiti određenim opcijama. Iz toga proizlazi ovaj pregled:
| Opcija | Učinak | Rizik kod stranog XML-a |
|---|---|---|
LIBXML_NOENT |
zamjenjuje entitete | prema priručniku može olakšati XXE napade |
LIBXML_DTDLOAD |
učitava vanjski podskup | aktivira učitavanje vanjskih entiteta |
LIBXML_DTDVALID |
validira prema DTD-u | može olakšati XXE napade |
LIBXML_NONET |
zabranjuje mrežne pristupe pri učitavanju | zaštitna dodatna opcija |
LIBXML_NO_XXE |
isključuje vanjske entitete pri zamjeni | zaštitna, od PHP-a 8.4 i libxml-a 2.13.0 |
LIBXML_PARSEHUGE |
ukida unutarnja ograničenja parsera | olakšava preopterećenje, samo za pouzdane podatke |
Zavaravajući naziv opcije LIBXML_NOENT poznata je zamka: zvuči kao „bez entiteta“, a djeluje suprotno.
Za razvojne inženjere to znači: rizično nije samo učitavanje XML-a, nego svako mjesto koje svjesno mijenja zadano ponašanje. Potraga za tim konstantama u kodu stoga je najbrži put do prve procjene, prije svake složenije provjere.
Sigurno obraniti XXE u PHP-u: uzorak za DOMDocument
Sljedeći primjer kombinira više zaštitnih slojeva. Postavlja Entity Loader koji odbija svaki vanjski resurs, učitava bez rizičnih opcija i odbija dokumente s vlastitim DTD-om:
<?php
declare(strict_types=1);
function ladeFremdesXml(string $xml): DOMDocument
{
// Vanjske resurse u načelu odbiti
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;
}
Prema PHP priručniku povratna vrijednost null u loaderu blokira učitavanje vanjskog resursa. OWASP upozorava da nije dovoljno null izravno predati funkciji libxml_set_external_entity_loader(); potrebna je funkcija koja vraća null. Za SimpleXML i XMLReader vrijede iste opcije. Gdje god je moguće, OWASP preporučuje potpuno odbijati DTD-ove.
Ako sami možete odrediti format podataka, JSON je za razmjenu između vlastitih sustava obično jednostavniji izbor. Za strane formate poput SAML-a ili Office datoteka XML ostaje neizbježan.
Pronaći skrivene XML ulaze
Mnoge aplikacije obrađuju više XML-a nego što njihovi razvojni inženjeri misle. Prođite ovaj popis:
- Sučelja: SOAP klijenti i poslužitelji, XML-RPC, usluge plaćanja i dostave s XML odgovorima.
- Prijava: Single Sign-On putem SAML-a temelji se na XML dokumentima koji dolaze izvana.
- Uvozi: podaci o proizvodima, feedovi, sitemapovi, formati kataloga.
- Uploadi: SVG slike su XML; Office formati poput DOCX i XLSX interno se sastoje od XML datoteka.
- Biblioteke: Composer paketi za dokumente, tablice ili SAML koji interno koriste libxml.
U kodu pomažu dvije pretrage:
grep -rnE "LIBXML_(NOENT|DTDLOAD|DTDVALID|PARSEHUGE)" src/ vendor/ --include="*.php"
grep -rnE "(loadXML|simplexml_load_(string|file)|XMLReader|->load\()" src/ --include="*.php"
Nalazišta u vendor/ nisu razlog za paniku, ali jesu povod da provjerite verziju i sigurnosne objave biblioteke.
Kod SVG uploada XXE je samo jedan od više rizika, jer SVG datoteke mogu sadržavati i skripte. Ako Vaša aplikacija taj format nužno ne treba, uopće ga ne dopuštajte za uploade. Inače SVG obrađujte samo sigurnim uzorkom iznad i datoteke isporučujte tako da preglednik ne izvršava sadržane skripte.
Pazite i na poruke o pogreškama. Ako aplikacija poruke libxml-a vraća korisnicima nefiltrirane, u njima mogu stajati dijelovi obrađivanog sadržaja. Pogreške pripadaju u zapisnik, korisnik dobiva opću poruku; u gornjem primjeru libxml_use_internal_errors(true) osigurava da upozorenja ne dospiju u izlaz. Ograničite i veličinu primljenih XML dokumenata prije nego ih parser uopće vidi.
Stari sustavi: kad nedostaje standardna zaštita
Zaštita zadanim ponašanjem ovisi o verziji libxml-a s kojom je PHP izgrađen. Koja verzija radi, pokazuje php -r 'echo LIBXML_DOTTED_VERSION, PHP_EOL;' na naredbenoj liniji ili ispis phpinfo() u zaštićenom okruženju. Vrijednost provjerite za svaku PHP verziju na poslužitelju, jer CLI i web-poslužitelj mogu koristiti različite buildove. Container imageovi donose vlastiti libxml; tamo provjeravajte u pokrenutom containeru, a ne na hostu.
Osobito su pogođeni sustavi koji ionako rade na starim PHP verzijama. Za PHP 8.1 i starije nakon PHP2Shella više nema službenih updatea PHP projekta; što to znači, opisuje PHP 8.1 i starije: što sada vrijedi. Stari sustav sa zastarjelim libxml-om i otvorenim ranjivostima interpretera ne bi trebalo samo osigurati u kodu, nego migrirati. Srodne rizike pri obradi stranih podataka opisuje Nesigurna deserijalizacija u PHP-u, a dohvate internih odredišta Spriječiti Server-Side Request Forgery u PHP-u.
Kako PHP SOS konkretno pomaže?
- Pronalazimo sva mjesta na kojima Vaša aplikacija i njezine biblioteke obrađuju XML iz stranih izvora.
- Uklanjamo rizične opcije parsera i ugrađujemo središnju, osiguranu funkciju učitavanja.
- Provjeravamo verzije libxml-a i PHP-a na svim poslužiteljima i kod starih sustava planiramo migraciju.
- Promjene testiramo bezopasnim probnim dokumentima i dokumentiramo rezultat.
Provjeru vlastitog softvera opisuje usluga Sigurnosni audit i penetracijski test.
Česta pitanja
Jesu li PHP aplikacije od PHP-a 8 automatski zaštićene od XXE-a?
U zadanom slučaju uglavnom jesu, jer libxml od verzije 2.9.0 više ne zamjenjuje entitete. Zaštita otpada čim kod postavi opcije poput LIBXML_NOENT, LIBXML_DTDLOAD ili LIBXML_DTDVALID ili je PHP izgrađen s vrlo starom verzijom libxml-a.
Trebam li još pozivati libxml_disable_entity_loader?
Ne. Funkcija je od PHP-a 8.0 zastarjela jer je učitavanje vanjskih entiteta zadano isključeno. Za dodatnu zaštitu PHP priručnik preporučuje libxml_set_external_entity_loader, a od PHP-a 8.4 uz odgovarajući libxml konstantu LIBXML_NO_XXE.
Gdje moja aplikacija uopće obrađuje XML?
Često na neočekivanim mjestima: SOAP sučelja, Single Sign-On putem SAML-a, RSS feedovi, sitemapovi, uvozi proizvoda, SVG uploadi i Office datoteke koje se interno sastoje od XML-a. I biblioteke mogu obrađivati XML a da to nije vidljivo u vlastitom kodu.
Što je napad Billion Laughs?
Dokument s ugniježđenim entitetima koji pri razrješavanju golemo naraste i troši memoriju i procesorsko vrijeme. Pripada istoj obitelji kao XXE. Zaštitu daju isključivanje zamjene entiteta i odbijanje dokumenata s vlastitim DTD-om.