Pe scurt: Preveniți XXE în PHP încărcând XML din surse externe fără opțiunile LIBXML_NOENT, LIBXML_DTDLOAD și LIBXML_DTDVALID, respingând documentele cu DTD propriu și setând un entity loader care refuză resursele externe. Începând cu libxml 2.9.0, PHP nu mai înlocuiește implicit entitățile, de aceea libxml_disable_entity_loader() este depășită începând cu PHP 8.0. Din PHP 8.4 cu libxml 2.13 este disponibilă în plus LIBXML_NO_XXE.
O vulnerabilitate veche care nu dispare
XML External Entities, pe scurt XXE, sunt considerate de mulți dezvoltatori un subiect încheiat. PHP a dezactivat implicit încărcarea entităților externe cu versiunile moderne de libxml, iar vechea funcție de protecție a fost marcată ca depășită cu PHP 8.0. Totuși, vulnerabilități XXE apar în continuare la verificări.
Motivul se află aproape întotdeauna într-una din trei situații: cineva a setat o opțiune pentru a rezolva o problemă de import și a reactivat astfel înlocuirea entităților. O bibliotecă face acest lucru intern. Sau aplicația rulează pe un sistem vechi, al cărui PHP a fost construit cu o libxml foarte veche.
XXE este periculos pentru că parserul XML încarcă atunci conținuturi doar referențiate în document: fișiere locale sau adrese din rețea. Consecințe posibile sunt citirea fișierelor de configurare, cereri către sisteme interne asemănătoare cu SSRF și suprasolicitarea serverului.
Ce se aplică din libxml 2.9 și PHP 8.0
Manualul PHP descrie situația astfel: începând cu libxml 2.9.0, înlocuirea entităților este dezactivată implicit. Entitățile externe nu mai trebuie de aceea dezactivate separat, în afară de cazul în care referințele de entități interne trebuie rezolvate cu anumite opțiuni. De aici rezultă această prezentare:
| Opțiune | Efect | Risc la XML străin |
|---|---|---|
LIBXML_NOENT |
înlocuiește entitățile | potrivit manualului, poate facilita atacurile XXE |
LIBXML_DTDLOAD |
încarcă subsetul extern | activează încărcarea entităților externe |
LIBXML_DTDVALID |
validează față de DTD | poate facilita atacurile XXE |
LIBXML_NONET |
interzice accesul la rețea la încărcare | opțiune suplimentară de protecție |
LIBXML_NO_XXE |
dezactivează entitățile externe la înlocuire | de protecție, din PHP 8.4 și libxml 2.13.0 |
LIBXML_PARSEHUGE |
ridică limitele interne ale parserului | facilitează suprasolicitarea, doar pentru date de încredere |
Numele înșelător al LIBXML_NOENT este o capcană cunoscută: sună a „fără entități”, dar produce opusul.
Pentru dezvoltatori aceasta înseamnă: riscantă nu este încărcarea XML în sine, ci orice loc care schimbă conștient comportamentul implicit. O căutare a acestor constante în cod este de aceea cea mai rapidă cale către o primă evaluare, înaintea oricărei verificări costisitoare.
Apărarea sigură împotriva XXE în PHP: un model pentru DOMDocument
Exemplul următor combină mai multe straturi de protecție. Setează un entity loader care refuză orice resursă externă, încarcă fără opțiuni riscante și respinge documentele cu DTD propriu:
<?php
declare(strict_types=1);
function ladeFremdesXml(string $xml): DOMDocument
{
// Refuzați în principiu resursele externe
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;
}
Potrivit manualului PHP, o valoare returnată null în loader blochează încărcarea resursei externe. OWASP atrage atenția că nu este suficient să transmiteți null direct către libxml_set_external_entity_loader(); este nevoie de o funcție care returnează null. Pentru SimpleXML și XMLReader se aplică aceleași opțiuni. Oriunde este posibil, OWASP recomandă respingerea completă a DTD-urilor.
Dacă puteți stabili singuri formatul datelor, JSON este de regulă alegerea mai simplă pentru schimbul dintre propriile sisteme. Pentru formate străine, precum SAML sau fișiere Office, XML rămâne inevitabil.
Găsirea intrărilor XML ascunse
Multe aplicații procesează mai mult XML decât cred dezvoltatorii lor. Parcurgeți această listă:
- Interfețe: clienți și servere SOAP, XML-RPC, servicii de plată și expediere cu răspunsuri XML.
- Autentificare: Single sign-on cu SAML se bazează pe documente XML care vin din exterior.
- Importuri: date de produse, feed-uri, sitemap-uri, formate de catalog.
- Upload-uri: Imaginile SVG sunt XML; formatele Office precum DOCX și XLSX constau intern din fișiere XML.
- Biblioteci: pachete Composer pentru documente, tabele sau SAML, care folosesc intern libxml.
În cod ajută două căutări:
grep -rnE "LIBXML_(NOENT|DTDLOAD|DTDVALID|PARSEHUGE)" src/ vendor/ --include="*.php"
grep -rnE "(loadXML|simplexml_load_(string|file)|XMLReader|->load\()" src/ --include="*.php"
Apariții în vendor/ nu sunt un motiv de panică, dar sunt un prilej de a verifica versiunea și avertismentele de securitate ale bibliotecii.
La upload-urile SVG, XXE este doar unul dintre mai multe riscuri, deoarece fișierele SVG pot conține și scripturi. Dacă aplicația dumneavoastră nu are neapărat nevoie de formatul respectiv, nu îl permiteți deloc la upload. Altfel, procesați SVG doar cu modelul sigur de mai sus și livrați fișierele astfel încât browserul să nu execute scripturile conținute.
Fiți atenți și la mesajele de eroare. Dacă o aplicație returnează utilizatorilor nefiltrate mesajele libxml, în ele pot apărea părți din conținutul procesat. Erorile aparțin jurnalului, iar utilizatorul primește un mesaj general; în exemplul de mai sus, libxml_use_internal_errors(true) face ca avertismentele să nu ajungă în ieșire. Limitați în plus dimensiunea documentelor XML primite, înainte ca parserul să le vadă.
Sisteme vechi: când lipsește protecția implicită
Protecția prin comportamentul implicit depinde de versiunea libxml cu care a fost construit PHP. Ce versiune rulează arată php -r 'echo LIBXML_DOTTED_VERSION, PHP_EOL;' în linia de comandă sau ieșirea phpinfo() într-un mediu protejat. Verificați valoarea pentru fiecare versiune PHP de pe server, deoarece CLI și serverul web pot folosi build-uri diferite. Imaginile de container aduc propria libxml; acolo verificați în containerul care rulează, nu pe host.
Sunt afectate în mod deosebit sistemele care oricum rulează pe versiuni PHP vechi. Pentru PHP 8.1 și mai vechi nu mai există, după PHP2Shell, actualizări oficiale din partea proiectului PHP; ce înseamnă aceasta descrie PHP 8.1 și mai vechi: ce se aplică acum. Un sistem vechi, cu libxml depășită și vulnerabilități deschise în interpretor, nu ar trebui doar securizat în cod, ci migrat. Riscuri înrudite la procesarea datelor străine descrie Deserializarea nesigură în PHP, iar apelurile către ținte interne Prevenirea Server-Side Request Forgery în PHP.
Cum ajută PHP SOS concret?
- Găsim toate locurile în care aplicația dumneavoastră și bibliotecile ei procesează XML din surse externe.
- Eliminăm opțiunile riscante ale parserului și introducem o funcție centrală de încărcare, securizată.
- Verificăm versiunile libxml și PHP pe toate serverele și planificăm migrarea la sistemele vechi.
- Testăm modificările cu documente de verificare inofensive și documentăm rezultatul.
Verificarea software-ului propriu o descrie serviciul Audit de securitate și test de penetrare.
Întrebări frecvente
Sunt aplicațiile PHP protejate automat de XXE începând cu PHP 8?
În cazul implicit, în mare măsură, deoarece libxml nu mai înlocuiește entitățile începând cu versiunea 2.9.0. Protecția dispare însă de îndată ce codul setează opțiuni precum LIBXML_NOENT, LIBXML_DTDLOAD sau LIBXML_DTDVALID sau PHP este construit cu o versiune libxml foarte veche.
Ar trebui să mai apelez libxml_disable_entity_loader?
Nu. Funcția este depășită începând cu PHP 8.0, deoarece încărcarea entităților externe este dezactivată implicit. Pentru protecție suplimentară, manualul PHP recomandă libxml_set_external_entity_loader și, începând cu PHP 8.4, cu o libxml potrivită, constanta LIBXML_NO_XXE.
Unde procesează, de fapt, aplicația mea XML?
Adesea în locuri neașteptate: interfețe SOAP, single sign-on cu SAML, feed-uri RSS, sitemap-uri, importuri de produse, upload-uri SVG și fișiere Office, care constau intern din XML. Și bibliotecile pot procesa XML fără ca acest lucru să fie vizibil în propriul cod.
Ce este atacul Billion Laughs?
Un document cu entități imbricate, care crește enorm la rezolvare și consumă memorie și timp de calcul. Face parte din aceeași familie ca XXE. Protecție oferă dezactivarea înlocuirii entităților și respingerea documentelor cu DTD propriu.