
A POC for the Apache Livy Unauthorized File Access Vunerability
For educational and security research purposes only. Do not use against systems you do not own or have explicit written permission to test. → Full Disclaimer
| Field | Detail |
|---|---|
| CVE ID | CVE-2025-60012 |
| Severity | Medium (CVSS 6.3) |
| Affected | Apache Livy 0.7.0-incubating, 0.8.0-incubating — when connected to Apache Spark 3.1 or later |
| Fixed in | Apache Livy 0.9.0-incubating |
| CWE | CWE-20: Improper Input Validation |
| Disclosed | 2026-03-13 |
| Reporter | Furue Hideyuki |
An authenticated user with access to Livy's REST or JDBC interface can submit a Spark session or batch job with crafted configuration values. Two weaknesses combine to allow the attacker to reference local filesystem files outside their permitted paths:
Missing validation for spark.archives — Spark 3.1 introduced spark.archives as a
unified way to distribute archive files across all cluster managers. Livy 0.8.0's hardcoded
list of configuration keys that get path-validated (HARDCODED_SPARK_FILE_LISTS) does not
include spark.archives. A path passed via this key is therefore never checked against the
local filesystem whitelist (livy.file.local-dir-whitelist), allowing an attacker to
reference any local file.
Path traversal bypass in whitelist check — Even for configuration keys that ARE
validated, the whitelist comparison in Livy 0.8.0 uses a plain Java String startsWith
call on the raw path. An attacker can bypass this using path traversal:
/whitelisted/dir/../../etc/passwd passes the string check but resolves outside the
allowed directory.
LivyConf.scalaVulnerable (v0.8.0): https://github.com/apache/incubator-livy/blob/v0.8.0-incubating/server/src/main/scala/org/apache/livy/LivyConf.scala
Fixed (v0.9.0): https://github.com/apache/incubator-livy/blob/v0.9.0-incubating/server/src/main/scala/org/apache/livy/LivyConf.scala
Session.scalaVulnerable (v0.8.0): https://github.com/apache/incubator-livy/blob/v0.8.0-incubating/server/src/main/scala/org/apache/livy/sessions/Session.scala
Fixed (v0.9.0): https://github.com/apache/incubator-livy/blob/v0.9.0-incubating/server/src/main/scala/org/apache/livy/sessions/Session.scala
Both versions were cloned directly from the official Apache Livy GitHub repository into this workspace using the following exact commands:
Repository: https://github.com/apache/incubator-livy
# Vulnerable version — cloned into ./livy-0.8.0/
git clone --depth=1 --branch v0.8.0-incubating \
https://github.com/apache/incubator-livy \
livy-0.8.0
# Fixed version — cloned into ./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 | 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 were produced by cloning both tags locally (see above) and running:
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 added to hardcoded file list private val HARDCODED_SPARK_FILE_LISTS = Seq(
SPARK_JARS,
SPARK_FILES,
SPARK_ARCHIVES,
SPARK_PY_FILES,
+ "spark.archives", // <-- ADDED in v0.9.0 (Spark 3.1+ config key)
"spark.yarn.archive",
"spark.yarn.dist.files",
"spark.yarn.dist.jars",
"spark.yarn.jar",
"spark.yarn.jars"
)
Impact of missing entry in v0.8.0:
When a user submits a session with conf: {"spark.archives": "file:///etc/passwd"}, Livy
0.8.0 never calls resolveURIs() on that value and never checks it against
livy.file.local-dir-whitelist. The path is forwarded to Spark unvalidated.
Session.scala: Path normalisation before whitelist check 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 in v0.8.0:
The raw string startsWith check can be bypassed with a path traversal payload.
Example: if livy.file.local-dir-whitelist = /opt/safe-data
/opt/safe-data/../../../etc/passwd
"/opt/safe-data/../../../etc/passwd".startsWith("/opt/safe-data") → true (bypassed)Paths.get("/opt/safe-data/../../../etc/passwd").normalize → /etc/passwd
/etc/passwd.startsWith(/opt/safe-data) → false (blocked)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
All steps in this PoC were executed and validated on the following system:
| Component | Detail |
|---|---|
| Host OS | Ubuntu 24.04.4 LTS (Noble Numbat) |
| Kernel | 6.17.0-14-generic x86_64 |
| Architecture | x86_64 |
| Total Memory | 15.49 GiB |
| Docker Engine | 28.2.2 |
| Host JDK | OpenJDK 17.0.18 (used by host only — containers use eclipse-temurin:11-jdk-focal) |
| Container base image | eclipse-temurin:11-jdk-focal (JDK 11, Ubuntu Focal) |
| Spark version (both images) | 3.1.3 with Hadoop 3.2 |
| Livy version — vulnerable image | 0.8.0-incubating (Scala 2.12 build) |
| Livy version — fixed image | 0.9.0-incubating (Scala 2.12 build) |
.
├── LICENSE
├── README.md
├── docker/
│ ├── fixed/
│ │ ├── Dockerfile
│ │ └── livy.conf
│ └── vulnerable/
│ ├── Dockerfile
│ └── livy.conf
├── livy-0.8.0/ ← Apache Livy 0.8.0-incubating source
├── livy-0.9.0/ ← Apache Livy 0.9.0-incubating source
└── test/
└── validate.sh
docker/vulnerable/ → image: cve-2025-60012-vulnerable (Livy 0.8.0 + Spark 3.1.3)
docker/fixed/ → image: cve-2025-60012-fixed (Livy 0.9.0 + Spark 3.1.3)
test/validate.sh → single script, run unchanged against both environments
Full end-to-end sequence — follow Steps 1 through 4 in order: