
POC для уязвимости неавторизованного доступа к файлам Apache Livy
Только для образовательных целей и исследований в области безопасности. Не используйте против систем, которыми вы не владеете или на тестирование которых не имеете явного письменного разрешения. → Полное предупреждение
| Поле | Детали |
|---|---|
| CVE ID | CVE-2025-60012 |
| Серьёзность | Средний (CVSS 6.3) |
| Затронутые версии | Apache Livy 0.7.0-incubating, 0.8.0-incubating — при подключении к Apache Spark 3.1 или новее |
| Исправлено в | Apache Livy 0.9.0-incubating |
| CWE | CWE-20: Некорректная проверка входных данных |
| Обнародована | 2026-03-13 |
| Сообщил | Furue Hideyuki |
Аутентифицированный пользователь с доступом к REST или JDBC интерфейсу Livy может отправить Spark-сессию или пакетное задание с подделанными значениями конфигурации. Две уязвимости в совокупности позволяют злоумышленнику ссылаться на файлы локальной файловой системы за пределами разрешённых путей:
Отсутствие проверки для spark.archives — Spark 3.1 ввёл spark.archives как унифицированный способ распространения архивных файлов по всем менеджерам кластера. Жёстко заданный список ключей конфигурации Livy 0.8.0, которые проходят проверку пути (HARDCODED_SPARK_FILE_LISTS), не включает spark.archives. Поэтому путь, переданный через этот ключ, никогда не проверяется по белому списку локальной файловой системы (livy.file.local-dir-whitelist), что позволяет злоумышленнику ссылаться на любой локальный файл.
Обход проверки белого списка через обход каталогов — Даже для ключей конфигурации, которые ПРОВЕРЯЮТСЯ, сравнение с белым списком в Livy 0.8.0 использует простой вызов startsWith Java String на необработанном пути. Злоумышленник может обойти это с помощью обхода каталогов: /whitelisted/dir/../../etc/passwd проходит строковую проверку, но разрешается за пределами разрешённого каталога.
LivyConf.scalaУязвимая (v0.8.0): https://github.com/apache/incubator-livy/blob/v0.8.0-incubating/server/src/main/scala/org/apache/livy/LivyConf.scala
Исправленная (v0.9.0): https://github.com/apache/incubator-livy/blob/v0.9.0-incubating/server/src/main/scala/org/apache/livy/LivyConf.scala
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```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/` |
## Точные различия кода
Различия были получены клонированием обоих тегов локально (см. выше) и выполнением:```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 добавлено в жестко заданный список файлов```diffprivate val HARDCODED_SPARK_FILE_LISTS = Seq( SPARK_JARS, SPARK_FILES, SPARK_ARCHIVES, SPARK_PY_FILES,
**Impact of missing entry in v0.8.0:**
Когда пользователь отправляет сессию с `conf: {"spark.archives": "file:///etc/passwd"}`, Livy
0.8.0 никогда не вызывает `resolveURIs()` для этого значения и не проверяет его по
`livy.file.local-dir-whitelist`. Путь передается Spark без проверки.
---
### Fix 2 — `Session.scala`: Нормализация пути перед проверкой белого списка```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.")
}
}
Влияние в v0.8.0:
Проверка сырой строки startsWith может быть обойдена с помощью полезной нагрузки обхода пути.
Пример: если `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** (обойдено)
- v0.9.0: `Paths.get("/opt/safe-data/../../../etc/passwd").normalize` → `/etc/passwd`
`/etc/passwd`.startsWith(`/opt/safe-data`) → **false** (заблокировано)
## Сводка векторов атак```
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
Все шаги в этом PoC были выполнены и проверены в следующей системе:
| Компонент | Подробности |
|---|---|
| Host OS | Ubuntu 24.04.4 LTS (Noble Numbat) |
| Kernel | 6.17.0-14-generic x86_64 |
| Architecture | x86_64 |
| Общая память | 15.49 GiB |
| Docker Engine | 28.2.2 |
| Host 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 (Scala 2.12 build) |
| Версия Livy — исправленный образ | 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
---
## Доказательство концепции