Un bug critic în PhpSpreadsheet a trecut direct prin propriul patch

A câteva săptămâni în urmă, echipa PHPOffice a lansat un patch pentru un bug de deserializare phar în PhpSpreadsheet, urmărit ca CVE-2026-34084. Cercetătorii au găsit apoi o cale de ocolire directă a acelui patch, iar bypass-ul a devenit propria vulnerabilitate: CVE-2026-45034, evaluată 9.2 critic pe CVSS v4. Avizul de securitate, GHSA-87m4-826x-3crx, a fost publicat pe 7 iunie. Dacă PhpSpreadsheet citește fișiere de tip spreadsheet oriunde în stack-ul tău, merită zece minute de atenție astăzi.
Patch-ul original a adăugat un helper numit File::prohibitWrappers, menit să respingă stream wrapper-e precum phar://. Acesta funcționa apelând parse_url($filename, PHP_URL_SCHEME) și lansând o excepție dacă schema returna un string mai lung de un caracter. Problema este că această verificare nu răspunde întrebării: „folosește path-ul acesta un wrapper?”. Dacă path-ul arată ca phar:///path/file.phar/inner, cu trei slash-uri după schemă, parse_url returnează boolean false în loc de string-ul schemei. Verificarea este sărită, helper-ul returnează fără niciun protest, iar IOFactory::load() ajunge tot la phar wrapper.
De ce versiunea PHP contează mai mult decât scorul CVSS
Ce se întâmplă mai departe depinde complet de versiunea ta de PHP, și aceasta este partea pe care echipele o înțeleg greșit. Pe PHP 7.x, simpla atingere a phar wrapper-ului prin is_file este suficientă pentru ca PHP să deserializeze metadatele phar-ului, ceea ce declanșează __wakeup și __destruct pe obiecte controlate de atacator. Acesta este RCE complet. Cercetătorii au demonstrat atacul pe branch-ul 1.x sub PHP 7.4 cu un lanț gadget care a scris un fișier martor. Pe PHP 8.x, deserializarea automată a metadatelor pentru operații simple pe fișiere a fost eliminată, astfel că la nivelul PhpSpreadsheet bug-ul coboară la o primitivă de citire phar. Execuția de cod revine doar dacă ceva din downstream-ul aplicației apelează Phar::getMetadata pe acel path.
Citește-l astfel. Ești încă pe PHP 7.x? Acesta este critic și urgent. Pe PHP 8.x nu ești automat în afara pericolului — ești la un singur apel Phar::getMetadata distanță de același rezultat, iar o citire arbitrară a fișierelor phar nu este un bug pe care vrei să-l ai la un endpoint de import de documente.
Ce spune asta despre cum sunt ocolite patch-urile
În angajamentele noastre de pentesting, deserializarea phar este unul dintre primele lucruri la care apelăm când o aplicație primește un path de fișier, un nume de fișier sau un upload și îl transmite unei biblioteci. Partea interesantă a acestui CVE nu este trucul cu wrapper-ul, ci forma patch-ului original. Patch-ul a validat schema pe care parse_url o returna, când regula reală era „acest path nu trebuie să facă referire la niciun wrapper”. Corectează simptomul vizibil și lași cazurile netestabile complet expuse. Trei slash-uri au fost suficiente.
Vedem același reflex în afara PHP. Din experiența noastră în PHP și Docker, construind aplicații cloud-native pentru clienți enterprise, bibliotecile care parsează input neîncredibil sunt cele care primesc o atenție suplimentară la fiecare actualizare de dependențe, nu doar când apare un CVE. În proiectele noastre native iOS și Android, același pattern se repetă cu SDK-uri third-party care acceptă în liniște un URL sau un path. Echipa are încredere în bibliotecă, biblioteca are încredere în input, și nimeni nu deține granița dintre ele. Un patch nou nu este sfârșitul unei clase de vulnerabilitate, ci un alt punct de date despre unde se află granița reală.
Ce să faci săptămâna aceasta
Trei pași concreți, în ordine:
- Actualizează PhpSpreadsheet. Fix-ul este 1.30.5 pe branch-ul 1.x sau release-ul patched de pe branch-ul pe care îl urmărești: 2.1.17, 2.4.6, 3.10.6 sau 5.8.0. Noul cod renunță la trucul cu
parse_urlși identificăphar://și wrapper-e similare cu pattern-uri explicite. - Caută în codebase-ul tău
IOFactory::loadșiReader::load, apoi urmărește fiecare apel înapoi la sursa path-ului. Orice apel accesibil dintr-un filename furnizat de utilizator, nume de upload sau URL este cel care contează. Fixează versiunea patchată înainte ca acel endpoint să fie livrat din nou. - Adaugă apărare în profunzime la nivelul PHP. Dezactivează stream wrapper-ele
phar://,php://,data://șiexpect://unde rulează codul tău de import, și validează sau canonicalizează orice path înainte ca acesta să ajungă la un loader. Dacă ești încă pe PHP 7.x, acest CVE este încă un motiv să termini acea migrare. Runtime-ul însuși a eliminat sink-ul de deserializare automată în PHP 8.
Dacă construiești în PHP, un obicei util este să tratezi fiecare bibliotecă de gestionare a fișierelor ca ostilă față de path-uri neîncredibile implicit, la fel cum facem noi când construim și revizuim servicii backend pentru clienți cu cerințe reale de conformitate. Costul acestui obicei este câteva minute per dependență. Costul omiterii lui este un RCE în pipeline-ul tău de documente.
Dacă o bibliotecă third-party care ajunge în liniște la phar:// în stack-ul tău PHP sună ca o problemă pe care ai prefera s-o prinzi înainte de un atacator, hai să discutăm.