
Un POC pour la vulnérabilité d'accès non autorisé aux fichiers 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 de tester. → Avertissement complet
| Champ | Détail |
|---|---|
| CVE ID | CVE-2025-60012 |
| Sévérité | Moyenne (CVSS 6.3) |
| Affecté | Apache Livy 0.7.0-incubating, 0.8.0-incubating — lorsqu'il est connecté à Apache Spark 3.1 ou ultérieur |
| Corrigé dans | Apache Livy 0.9.0-incubating |
| CWE | CWE-20: Validation d'entrée incorrecte |
| Divulgué | 2026-03-13 |
| Signaleur | Furue Hideyuki |
Un utilisateur authentifié ayant accès à l'interface REST ou JDBC de Livy peut soumettre une session Spark ou un job batch avec des valeurs de configuration malveillantes. Deux faiblesses combinées permettent à l'attaquant de référencer des fichiers du système de fichiers local en dehors des chemins autorisés :
Absence de validation pour spark.archives — Spark 3.1 a introduit spark.archives comme un moyen unifié de distribuer des fichiers d'archive sur tous les gestionnaires de cluster. La liste codée en dur de Livy 0.8.0 des clés de configuration qui sont validées par chemin (HARDCODED_SPARK_FILE_LISTS) n'inclut pas spark.archives. Un chemin passé via cette clé n'est donc jamais vérifié par rapport à la liste blanche du système de fichiers local (livy.file.local-dir-whitelist), permettant à un attaquant de référencer n'importe quel fichier local.
Contournement de traversée de chemin dans la vérification de la liste blanche — Même pour les clés de configuration qui SONT validées, la comparaison de la liste blanche dans Livy 0.8.0 utilise un simple appel startsWith de chaîne Java sur le chemin brut. Un attaquant peut contourner cela en utilisant une traversée de chemin : /whitelisted/dir/../../etc/passwd passe la vérification de chaîne mais se résout en dehors du répertoire autorisé.
LivyConf.scalaVulnérable (v0.8.0): https://github.com/apache/incubator-livy/blob/v0.8.0-incubating/server/src/main/scala/org/apache/livy/LivyConf.scala
Corrigé (v0.9.0): https://github.com/apache/incubator-livy/blob/v0.9.0-incubating/server/src/main/scala/org/apache/livy/LivyConf.scala
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 dans cet espace de travail en utilisant les commandes exactes suivantes :
Dépôt : https://github.com/apache/incubator-livy```bash
git clone --depth=1 --branch v0.8.0-incubating
https://github.com/apache/incubator-livy
livy-0.8.0
git clone --depth=1 --branch v0.9.0-incubating
https://github.com/apache/incubator-livy
livy-0.9.0
| Version | Tag | Resolved commit | Local path |
|---------|-----|-----------------|------------|
| 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/` |
## Diffs exacts du code
Les diffs ont été produits en clonant les deux tags localement (voir ci-dessus) et en exécutant :```bash
diff -u livy-0.8.0/server/src/main/scala/org/apache/livy/LivyConf.scala \
livy-0.9.0/server/src/main/scala/org/apache/livy/LivyConf.scala
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
LivyConf.scala: spark.archives ajouté à la liste de fichiers codée en dur```diffprivate val HARDCODED_SPARK_FILE_LISTS = Seq( SPARK_JARS, SPARK_FILES, SPARK_ARCHIVES, SPARK_PY_FILES,
**Impact de l'entrée manquante dans v0.8.0 :**
Lorsqu'un utilisateur soumet une session avec `conf: {"spark.archives": "file:///etc/passwd"}`, Livy
0.8.0 n'appelle jamais `resolveURIs()` sur cette valeur et ne la vérifie jamais par rapport à
`livy.file.local-dir-whitelist`. Le chemin est transmis à Spark sans validation.
---
### Correctif 2 — `Session.scala` : Normalisation du chemin avant la vérification de la liste blanche```diff
def resolveURI(uri: URI, livyConf: LivyConf): URI = {
...
if (resolved.getScheme() == "file") {
- require(livyConf.localFsWhitelist.find(resolved.getPath().startsWith).isDefined,
+ require(livyConf.localFsWhitelist.find(
+ Paths.get(resolved.getPath()).normalize.startsWith).isDefined,
s"Local path ${uri.getPath()} cannot be added to user sessions.")
}
}
Impact dans v0.8.0:
La vérification de chaîne brute startsWith peut être contournée avec une charge utile de traversée de chemin.
Exemple : si `livy.file.local-dir-whitelist = /opt/safe-data```` /opt/safe-data/../../../etc/passwd
- v0.8.0: `"/opt/safe-data/../../../etc/passwd".startsWith("/opt/safe-data")` → **true** (contourné)
- v0.9.0: `Paths.get("/opt/safe-data/../../../etc/passwd").normalize` → `/etc/passwd`
`/etc/passwd`.startsWith(`/opt/safe-data`) → **false** (bloqué)
## Résumé des vecteurs d'attaque```
Attacker (authenticated REST/JDBC user)
│
▼
POST /sessions (or /batches)
{
"conf": {
"spark.archives": "file:///etc/shadow" ← Attack 1: unvalidated Spark 3.1 key
"spark.jars": "file:///safe/../etc/shadow" ← Attack 2: path traversal bypass
}
}
│
▼
Livy 0.8.0 — validation skipped / bypassed
│
▼
Spark reads the file and distributes it to executors
│
▼
Attacker retrieves file contents via job output / 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.49 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 with Hadoop 3.2 |
| Version Livy — image vulnérable | 0.8.0-incubating (Scala 2.12 build) |
| Version Livy — image corrigée | 0.9.0-incubating (Scala 2.12 build) |