Kort sagt: XXE i PHP förhindrar ni genom att ladda XML från främmande källor utan alternativen LIBXML_NOENT, LIBXML_DTDLOAD och LIBXML_DTDVALID, avvisa dokument med egen DTD och sätta en entity-loader som nekar externa resurser. Sedan libxml 2.9.0 ersätter PHP inte längre entities som standard, därför är libxml_disable_entity_loader() föråldrad sedan PHP 8.0. Från PHP 8.4 med libxml 2.13 finns dessutom LIBXML_NO_XXE.
En gammal sårbarhet som inte försvinner
XML External Entities, kort XXE, anses av många utvecklare vara ett avklarat ämne. PHP har med moderna libxml-versioner stängt av laddning av externa entities som standard, och den gamla skyddsfunktionen markerades som föråldrad med PHP 8.0. Ändå dyker XXE-sårbarheter fortfarande upp vid granskningar.
Orsaken är nästan alltid en av tre situationer: Någon har satt ett alternativ för att lösa ett importproblem och därmed slagit på entity-ersättningen igen. Ett bibliotek gör det internt. Eller så körs applikationen på ett äldre system vars PHP byggdes med en mycket gammal libxml.
XXE är farligt eftersom XML-parsern då laddar in innehåll som i dokumentet bara refereras: lokala filer eller adresser i nätet. Möjliga följder är att konfigurationsfiler läses ut, förfrågningar till interna system liknande SSRF och överbelastning av servern.
Vad som gäller sedan libxml 2.9 och PHP 8.0
PHP-manualen beskriver läget så här: Från libxml 2.9.0 är ersättning av entities avaktiverad som standard. Externa entities behöver därför inte längre stängas av särskilt, såvida inte interna entity-referenser ska lösas upp med vissa alternativ. Av det följer den här översikten:
| Alternativ | Verkan | Risk vid främmande XML |
|---|---|---|
LIBXML_NOENT |
ersätter entities | kan enligt manualen underlätta XXE-angrepp |
LIBXML_DTDLOAD |
laddar den externa delmängden | aktiverar laddning av externa entities |
LIBXML_DTDVALID |
validerar mot DTD:n | kan underlätta XXE-angrepp |
LIBXML_NONET |
förbjuder nätverksåtkomst vid laddning | skyddande tilläggsalternativ |
LIBXML_NO_XXE |
stänger av externa entities vid ersättning | skyddande, från PHP 8.4 och libxml 2.13.0 |
LIBXML_PARSEHUGE |
upphäver parserns interna gränser | underlättar överbelastning, bara för betrodda data |
Det vilseledande namnet på LIBXML_NOENT är en känd fälla: Det låter som ”inga entities” men gör motsatsen.
För utvecklare innebär det: Riskabelt är inte att ladda XML i sig utan varje ställe som medvetet ändrar standardbeteendet. En sökning efter dessa konstanter i koden är därför den snabbaste vägen till en första bedömning, före varje omständlig granskning.
Avvärja XXE i PHP säkert: ett mönster för DOMDocument
Följande exempel kombinerar flera skyddslager. Det sätter en entity-loader som nekar varje extern resurs, laddar utan riskabla alternativ och avvisar dokument med egen DTD:
<?php
declare(strict_types=1);
function ladeFremdesXml(string $xml): DOMDocument
{
// Neka externa resurser i princip
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;
}
Enligt PHP-manualen blockerar returvärdet null i loadern laddningen av den externa resursen. OWASP påpekar att det inte räcker att lämna över null direkt till libxml_set_external_entity_loader(); det krävs en funktion som returnerar null. För SimpleXML och XMLReader gäller samma alternativ. Där det är möjligt rekommenderar OWASP att avvisa DTD:er helt.
Kan ni själva bestämma dataformatet är JSON för utbyte mellan egna system oftast det enklare valet. För främmande format som SAML eller Office-filer är XML oundvikligt.
Hitta dolda XML-ingångar
Många applikationer bearbetar mer XML än deras utvecklare tror. Gå igenom den här listan:
- Gränssnitt: SOAP-klienter och -servrar, XML-RPC, betal- och fraktleverantörer med XML-svar.
- Inloggning: Single sign-on med SAML bygger på XML-dokument som kommer utifrån.
- Importer: Produktdata, flöden, sitemaps, katalogformat.
- Uppladdningar: SVG-bilder är XML; Office-format som DOCX och XLSX består internt av XML-filer.
- Bibliotek: Composer-paket för dokument, tabeller eller SAML som internt använder libxml.
I koden hjälper två sökningar:
grep -rnE "LIBXML_(NOENT|DTDLOAD|DTDVALID|PARSEHUGE)" src/ vendor/ --include="*.php"
grep -rnE "(loadXML|simplexml_load_(string|file)|XMLReader|->load\()" src/ --include="*.php"
Fynd i vendor/ är ingen anledning till panik, men en anledning att kontrollera bibliotekets version och säkerhetsmeddelanden.
Vid SVG-uppladdningar är XXE bara en av flera risker, eftersom SVG-filer också kan innehålla skript. Behöver er applikation inte formatet tvunget, tillåt det inte alls för uppladdningar. Annars bearbetar ni SVG bara med det säkra mönstret ovan och levererar filerna så att webbläsaren inte kör inneslutna skript.
Se också upp med felmeddelanden. Returnerar en applikation libxmls meddelanden ofiltrerat till användare kan där stå delar av det bearbetade innehållet. Fel hör hemma i loggen, användaren får ett allmänt meddelande; i exemplet ovan ser libxml_use_internal_errors(true) till att varningar inte hamnar i utdata. Begränsa dessutom storleken på mottagna XML-dokument innan parsern över huvud taget ser dem.
Äldre system: när standardskyddet saknas
Skyddet genom standardbeteendet hänger på den libxml-version som PHP byggdes med. Vilken version som körs visar php -r 'echo LIBXML_DOTTED_VERSION, PHP_EOL;' på kommandoraden eller utdata från phpinfo() i en skyddad miljö. Kontrollera värdet för varje PHP-version på servern, eftersom CLI och webbserver kan använda olika byggen. Containerbilder har sin egen libxml med sig; där kontrollerar ni i den körande containern, inte på värden.
Särskilt berörda är system som redan körs på gamla PHP-versioner. För PHP 8.1 och äldre finns efter PHP2Shell inga officiella uppdateringar från PHP-projektet längre; vad det innebär beskriver PHP 8.1 och äldre: vad som gäller nu. Ett äldre system med föråldrad libxml och öppna tolksårbarheter bör inte bara säkras i koden utan migreras. Närliggande risker vid bearbetning av främmande data beskriver Osäker deserialisering i PHP, hämtningar av interna mål Förhindra Server-Side Request Forgery i PHP.
Hur hjälper PHP SOS konkret?
- Vi hittar alla ställen där er applikation och dess bibliotek bearbetar XML från främmande källor.
- Vi tar bort riskabla parseralternativ och bygger in en central, säkrad laddningsfunktion.
- Vi kontrollerar libxml- och PHP-versioner på alla servrar och planerar migreringen för äldre system.
- Vi testar ändringarna med ofarliga testdokument och dokumenterar resultatet.
Granskning av egen programvara beskriver tjänsten säkerhetsaudit och penetrationstest.
Vanliga frågor
Är PHP-applikationer automatiskt skyddade mot XXE sedan PHP 8?
I standardfallet i stort sett, eftersom libxml från version 2.9.0 inte längre ersätter entities. Skyddet faller dock bort så snart koden sätter alternativ som LIBXML_NOENT, LIBXML_DTDLOAD eller LIBXML_DTDVALID eller PHP är byggd med en mycket gammal libxml-version.
Ska jag fortfarande anropa libxml_disable_entity_loader?
Nej. Funktionen är föråldrad sedan PHP 8.0, eftersom laddning av externa entities är avstängd som standard. För ytterligare skydd rekommenderar PHP-manualen libxml_set_external_entity_loader och från PHP 8.4 med lämplig libxml konstanten LIBXML_NO_XXE.
Var bearbetar min applikation över huvud taget XML?
Ofta på oväntade ställen: SOAP-gränssnitt, single sign-on med SAML, RSS-flöden, sitemaps, produktimporter, SVG-uppladdningar och Office-filer, som internt består av XML. Även bibliotek kan bearbeta XML utan att det syns i den egna koden.
Vad är Billion-Laughs-attacken?
Ett dokument med nästlade entities som växer enormt när de löses upp och förbrukar minne och beräkningstid. Den tillhör samma familj som XXE. Skydd ger avstängd entity-ersättning och avvisande av dokument med egen DTD.