
Une preuve de concept pour la vulnérabilité de contournement de la liste blanche de traversée de chemin d'Apache Livy
À des fins éducatives et de recherche en sécurité uniquement. Ne pas utiliser contre des systèmes que vous ne possédez pas ou pour lesquels vous n'avez pas d'autorisation écrite explicite. → Clause de non-responsabilité complète
| Champ | Détail |
|---|---|
| ID CVE | CVE-2025-66249 |
| Sévérité | Important (CVSS N/A — évaluation NVD en attente au 15/03/2026) |
| Affecté | Apache Livy 0.3.0-incubating jusqu'à 0.8.0-incubating — uniquement lorsque livy.file.local-dir-whitelist est défini sur une valeur non par défaut |
| Corrigé dans | Apache Livy 0.9.0-incubating |
| CWE | CWE-22 : Limitation incorrecte d'un chemin d'accès à un répertoire restreint ('Traversal de chemin') |
| Divulgué | 2026-03-12 (OSS-Sec) / 2026-03-13 (NVD) |
| Rapporteur | Hiroki Egawa (découvreur) |
Un utilisateur authentifié ayant accès à l'interface REST ou JDBC de Livy peut soumettre une session Spark ou un job batch avec une valeur de configuration de chemin de fichier conçue pour échapper à la liste blanche des répertoires autorisés.
Cause racine — Contournement du traversal de chemin dans le contrôle de la liste blanche (Session.scala)
Lorsque livy.file.local-dir-whitelist est configuré, Livy 0.8.0 valide les chemins soumis en appelant String.startsWith() de Java sur le chemin brut, non normalisé. Ce contrôle peut être contourné en utilisant des séquences de traversal ../ :
/opt/safe-data/../sensitive/secret.txt
La chaîne brute commence par /opt/safe-data, donc le contrôle passe — mais le chemin se résout en /opt/sensitive/secret.txt, qui est totalement en dehors du répertoire autorisé.
Condition de déclenchement : La vulnérabilité ne peut être exploitée que lorsque livy.file.local-dir-whitelist est défini sur une valeur non par défaut (non vide). Si la liste blanche est vide (par défaut), la validation de chemin est totalement ignorée et le problème n'est pas actif.
Impact : Un attaquant qui soumet une session via l'API REST de Livy peut référencer des fichiers locaux arbitraires sur le serveur Livy hôte. Dans un cluster d'analyse partagé, cela se traduit par une exposition potentielle des identifiants, clés, fichiers de configuration, ou toute donnée lisible par l'utilisateur du processus Livy.
Session.scalaVulnérable (v0.8.0) : https://github.com/apache/incubator-livy/blob/v0.8.0-incubating/server/src/main/scala/org/apache/livy/sessions/Session.scala
Corrigé (v0.9.0) : https://github.com/apache/incubator-livy/blob/v0.9.0-incubating/server/src/main/scala/org/apache/livy/sessions/Session.scala
Les deux versions ont été clonées directement depuis le dépôt GitHub officiel d'Apache Livy en utilisant les commandes exactes suivantes :
Dépôt : https://github.com/apache/incubator-livy
# Version vulnérable — clonée dans ./livy-0.8.0/
git clone --depth=1 --branch v0.8.0-incubating \
https://github.com/apache/incubator-livy \
livy-0.8.0
# Version corrigée — clonée dans ./livy-0.9.0/
git clone --depth=1 --branch v0.9.0-incubating \
https://github.com/apache/incubator-livy \
livy-0.9.0
| Version | Tag | Commit résolu | Chemin local |
|---|---|---|---|
| 0.8.0-incubating | v0.8.0-incubating | 78b512658e4baf1183f2b352203ada1928d8111a | ./livy-0.8.0/ |
| 0.9.0-incubating | v0.9.0-incubating | 7215f209b25b96488189567807eaded00953a492 | ./livy-0.9.0/ |
Session.scala : Paths.get().normalize() avant le contrôle de liste blanche import java.io.InputStream
import java.net.{URI, URISyntaxException}
+import java.nio.file.Paths
import java.security.PrivilegedExceptionAction
+import java.util.concurrent.{Executors, LinkedBlockingQueue, ThreadFactory, ThreadPoolExecutor, TimeUnit}
import java.util.UUID
...
if (resolved.getScheme() == "file") {
// Assurer que l'emplacement est dans la liste blanche avant d'autoriser l'ajout de fichiers locaux.
- require(livyConf.localFsWhitelist.find(resolved.getPath().startsWith).isDefined,
+ require(livyConf.localFsWhitelist.find(
+ Paths.get(resolved.getPath()).normalize.startsWith).isDefined,
s"Le chemin local ${uri.getPath()} ne peut pas être ajouté aux sessions utilisateur.")
}
Impact dans v0.8.0 :
Le contrôle startsWith sur la chaîne brute peut être contourné avec une charge utile de traversal de chemin.
Exemple : si livy.file.local-dir-whitelist = /opt/safe-data
/opt/safe-data/../sensitive/secret.txt
"/opt/safe-data/../sensitive/secret.txt".startsWith("/opt/safe-data") → vrai (contourné)Paths.get("/opt/safe-data/../sensitive/secret.txt").normalize → /opt/sensitive/secret.txt
/opt/sensitive/secret.txt.startsWith(/opt/safe-data) → faux (bloqué)Les diffs ont été produits en clonant les deux tags localement (voir ci-dessus) et en exécutant :
diff -u \
livy-0.8.0/server/src/main/scala/org/apache/livy/sessions/Session.scala \
livy-0.9.0/server/src/main/scala/org/apache/livy/sessions/Session.scala
Attaquant (utilisateur REST/JDBC authentifié)
│
▼
POST /sessions
{
"conf": {
"spark.jars": "file:///opt/safe-data/../sensitive/secret.txt"
← le chemin commence par le préfixe autorisé — String.startsWith() passe
← mais se résout EN DEHORS du répertoire via le traversal ../
}
}
│
▼
Livy 0.8.0 — contrôle de liste blanche contourné (startsWith brut, sans normalisation)
│
▼
Spark lit le fichier et le distribue aux exécuteurs
│
▼
L'attaquant récupère le contenu du fichier via la sortie du job / les logs
Toutes les étapes de cette PoC ont été exécutées et validées sur le système suivant :
| Composant | Détail |
|---|---|
| OS hôte | Ubuntu 24.04.4 LTS (Noble Numbat) |
| Noyau | 6.17.0-14-generic x86_64 |
| Architecture | x86_64 |
| Mémoire totale | 15 GiB |
| Moteur Docker | 28.2.2 |
| JDK hôte | OpenJDK 17.0.18 (utilisé uniquement par l'hôte — les conteneurs utilisent eclipse-temurin:11-jdk-focal) |
| Image de base du conteneur | eclipse-temurin:11-jdk-focal (JDK 11, Ubuntu Focal) |
| Version Spark (les deux images) | 3.1.3 avec Hadoop 3.2 |
| Version Livy — image vulnérable | 0.8.0-incubating |
| Version Livy — image corrigée | 0.9.0-incubating |
CVE-2025-66249-POC/
├── docker/
│ ├── fixed/
│ │ ├── Dockerfile
│ │ ├── livy.conf
│ │ └── start.sh
│ └── vulnerable/
│ ├── Dockerfile
│ ├── livy.conf
│ └── start.sh
├── test/
│ └── validate.sh
├── .gitignore
├── LICENSE
└── README.md