PhpSpreadsheet est l'une des bibliothèques PHP les plus déployées au monde : utilisée par des millions de sites pour importer/exporter Excel, OpenDocument et CSV. CVE-2026-34084 (CVSS 9.8) révèle que la fonction IOFactory::load() accepte des chemins wrappers PHP (phar://, ftp://, ssh2.sftp://) qui passent le contrôle File::assertFile(). Le wrapper phar:// déclenche la désérialisation des métadonnées PHAR — chemin d'attaque classique vers une RCE PHP. Les wrappers ftp:// et ssh2.sftp:// ouvrent eux la voie à du SSRF côté serveur.
Pour tout site qui passe une donnée utilisateur (chemin de fichier, URL d'import, paramètre d'upload) à IOFactory::load(), c'est un boulevard pour la compromission complète.
Détails techniques
Composant vulnérable
La méthode IOFactory::load(string $filename, ...) est le point d'entrée standard pour charger un fichier tableur. Elle commence par appeler File::assertFile($filename) pour vérifier que le chemin est valide. Cette vérification utilise is_file() — qui retourne true pour les chemins wrapper PHP comme phar://, ftp://, ssh2.sftp://.
Une fois la vérification passée, PhpSpreadsheet ouvre le fichier via les fonctions PHP standard. Si le chemin est phar://archive.phar, PHP désérialise automatiquement les métadonnées de l'archive PHAR, ce qui invoque potentiellement des méthodes magiques (__wakeup, __destruct) sur des objets dont la classe est définie dans le code de l'application.
C'est le pattern classique de gadget chain PHP : si l'application contient une classe avec une méthode magique exploitable (ex: eval($this->code) dans __destruct), l'attaquant exécute du code arbitraire.
Caractéristiques
| Champ | Valeur |
|---|---|
| CVSS 3.1 | 9.8 (CRITICAL) |
| Vecteur | AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-502 (Deserialization of Untrusted Data) + CWE-918 (SSRF) |
| Pré-requis | Le paramètre $filename de IOFactory::load() doit être influençable par l'utilisateur |
| Pré-requis #2 pour RCE | Présence d'une gadget chain dans le code (souvent fournie par les frameworks Laravel/Symfony/Yii/CodeIgniter et leurs dépendances) |
Produits et versions affectés
| Branche | Versions affectées | Version corrigée |
|---|---|---|
| 1.x | ≤ 1.30.2 | 1.30.3 |
| 2.0.x | 2.0.0 → 2.1.14 | 2.1.15 |
| 2.2.x → 2.4.x | 2.2.0 → 2.4.3 | 2.4.4 |
| 3.3.x → 3.10.x | 3.3.0 → 3.10.3 | 3.10.4 |
| 4.x → 5.x | 4.0.0 → 5.5.0 | 5.6.0 |
Vérifiez votre version :
composer show phpoffice/phpspreadsheet | grep versions
Exploitation et impact
Scénarios d'attaque
Scénario 1 — Import Excel utilisateur
Une application permet aux utilisateurs d'importer un fichier Excel pour traitement. Le contrôleur reçoit le path stocké et appelle IOFactory::load($uploadedFile). L'attaquant uploade un fichier d'apparence légitime mais nommé evil.phar (ou via path traversal force le wrapper phar://).
Scénario 2 — Paramètre URL contrôlé
L'application accepte un paramètre ?file=... pour générer un rapport à partir d'un template. L'attaquant fournit ?file=phar:///tmp/uploaded.phar et déclenche la désérialisation.
Scénario 3 — SSRF via ftp/ssh2.sftp
Même sans gadget chain RCE, un attaquant peut utiliser ftp://internal-service:21/file.xlsx pour faire émettre des requêtes par le serveur PHP vers des services internes inaccessibles depuis Internet — typique pour attaquer une instance Redis, un metadata service cloud (AWS IMDS), ou un dashboard interne.
PoC concept (RCE via phar)
// Côté attaquant — fabrication du PHAR avec gadget chain
$phar = new Phar('evil.phar');
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($gadgetChainObject); // gadget de l'application cible
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
// Le fichier evil.phar est ensuite uploadé sur la cible
// Côté cible (vulnérable)
$file = $_GET['file']; // attacker controlled
\PhpOffice\PhpSpreadsheet\IOFactory::load($file);
// si $file = 'phar:///tmp/evil.phar', la gadget chain s'exécute
Conséquences
- RCE complète dans le processus PHP (souvent www-data ou compte applicatif)
- SSRF vers les services internes non exposés
- Lecture de fichiers via
phar://pointant sur des chemins arbitraires - Pivot LAN depuis le serveur web
Détection et IOC
Logs serveur web
Recherchez dans les access logs des paramètres contenant phar://, ftp://, ssh2.sftp:// :
grep -E "phar://|ftp://|ssh2\.sftp://" /var/log/apache2/access.log
Logs PHP
Activez le logging des erreurs et chassez :
- Warnings de désérialisation
- Erreurs
PharException - Fatal errors
__wakeup,__destruct
grep -E "Phar|unserialize|__wakeup|__destruct" /var/log/php/error.log
Audit du code applicatif
Localisez tous les appels à IOFactory::load() :
grep -rn "IOFactory::load" /var/www/your-app/
Pour chaque appel, vérifiez :
- Le
$filenameest-il dérivé d'une entrée utilisateur (POST, GET, COOKIE, header) ? - Y a-t-il une validation explicite du préfixe (rejet de
phar://, etc.) ?
Règle WAF (ModSecurity)
SecRule ARGS "@rx (phar|ftp|ssh2\.sftp)://" \
"id:'2026034084',phase:2,deny,status:403,\
msg:'PhpSpreadsheet CVE-2026-34084 wrapper injection attempt',\
t:lowercase"
Mitigation et patch
Action immédiate : mise à jour
# Composer
composer require phpoffice/phpspreadsheet:^5.6.0
# Ou pour les branches plus anciennes :
composer require phpoffice/phpspreadsheet:^3.10.4
composer require phpoffice/phpspreadsheet:^2.4.4
composer require phpoffice/phpspreadsheet:^2.1.15
composer require phpoffice/phpspreadsheet:^1.30.3
Workaround si mise à jour impossible
- Validation côté application avant tout appel à
IOFactory::load():
function isSafeSpreadsheetPath(string $path): bool {
// Refuser tous les wrappers PHP
$forbidden = ['phar://', 'ftp://', 'ssh2.sftp://', 'http://', 'https://',
'data://', 'php://', 'zip://', 'compress.zlib://'];
foreach ($forbidden as $prefix) {
if (stripos($path, $prefix) === 0) return false;
}
// Forcer un chemin absolu dans un répertoire de confiance
$real = realpath($path);
return $real !== false && str_starts_with($real, '/var/uploads/');
}
if (!isSafeSpreadsheetPath($filename)) {
throw new InvalidArgumentException("Unsafe path");
}
\PhpOffice\PhpSpreadsheet\IOFactory::load($filename);
- Désactiver l'extension PHAR dans
php.inisi elle n'est pas utilisée par votre application :
disable_functions = phar_loader
; ou supprimer l'extension phar du chargement
- WAF avec règle de filtrage des wrappers PHP en entrée
Hardening durable
- Auditer toutes les bibliothèques PHP qui appellent
is_file(),file_exists(),file_get_contents()sur des chemins user-controlled (PhpSpreadsheet n'est pas seul à avoir ce pattern) - Bloquer au niveau Apache/Nginx les requêtes contenant
phar://,ftp://, etc. dans les paramètres - Faire tourner les credentials applicatifs si vous suspectez une exploitation passée
- Auditer les logs PHP pour des warnings de désérialisation anciens
Pourquoi surveiller en continu vos dépendances applicatives
PhpSpreadsheet est une de ces dépendances qu'on installe une fois et qu'on oublie — pourtant elle est embarquée dans des milliers de projets PHP, incluant des CMS, des outils internes, des plugins WordPress. Une CVE comme CVE-2026-34084 affecte instantanément l'ensemble de ces déploiements, mais peu de DSI ont une visibilité sur leurs dépendances tierces.
Avec cveo.tech, inventoriez vos applications PHP et leurs versions de dépendances clés et soyez alerté automatiquement dès qu'une CVE critique cible une de vos versions exactes — y compris sur les composants enfouis dans le composer.lock que personne ne regarde plus.