
Uma PoC para a vulnerabilidade de acesso não autorizado a arquivos no Apache Livy.
Apenas para fins educacionais e de pesquisa em segurança. Não utilize contra sistemas que você não possui ou para os quais não tenha permissão explícita por escrito para testar. → Isenção de Responsabilidade Completa
| Campo | Detalhe |
|---|---|
| CVE ID | CVE-2025-60012 |
| Severidade | Média (CVSS 6.3) |
| Afetados | Apache Livy 0.7.0-incubating, 0.8.0-incubating — quando conectado ao Apache Spark 3.1 ou posterior |
| Corrigido em | Apache Livy 0.9.0-incubating |
| CWE | CWE-20: Validação de Entrada Incorreta |
| Divulgado | 2026-03-13 |
| Relator | Furue Hideyuki |
Um usuário autenticado com acesso à interface REST ou JDBC do Livy pode enviar uma sessão Spark ou um job em lote com valores de configuração manipulados. Duas falhas combinam-se para permitir que o atacante referencie arquivos do sistema de arquivos local fora dos caminhos permitidos:
Ausência de validação para spark.archives — O Spark 3.1 introduziu spark.archives
como uma forma unificada de distribuir arquivos compactados por todos os gerenciadores de
cluster. A lista fixa de chaves de configuração do Livy 0.8.0 que recebem validação de
caminho (HARDCODED_SPARK_FILE_LISTS) não inclui spark.archives. Portanto, um caminho
passado por meio dessa chave nunca é verificado contra a lista de permissões do sistema de
arquivos local (livy.file.local-dir-whitelist), permitindo que um atacante referencie
qualquer arquivo local.
Contorno da verificação da lista de permissões por path traversal — Mesmo para chaves
de configuração que SÃO validadas, a comparação com a lista de permissões no Livy 0.8.0 usa
uma simples chamada startsWith de String do Java no caminho bruto. Um atacante pode contornar
isso usando path traversal: /whitelisted/dir/../../etc/passwd passa na verificação da string,
mas resolve para fora do diretório permitido.
LivyConf.scalaVulnerável (v0.8.0): https://github.com/apache/incubator-livy/blob/v0.8.0-incubating/server/src/main/scala/org/apache/livy/LivyConf.scala
Corrigido (v0.9.0): https://github.com/apache/incubator-livy/blob/v0.9.0-incubating/server/src/main/scala/org/apache/livy/LivyConf.scala
Session.scalaVulnerável (v0.8.0): https://github.com/apache/incubator-livy/blob/v0.8.0-incubating/server/src/main/scala/org/apache/livy/sessions/Session.scala
Corrigido (v0.9.0): https://github.com/apache/incubator-livy/blob/v0.9.0-incubating/server/src/main/scala/org/apache/livy/sessions/Session.scala
Ambas as versões foram clonadas diretamente do repositório oficial Apache Livy no GitHub para este workspace usando os seguintes comandos exatos:
Repositório: 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 de código exatos
Os diffs foram produzidos clonando ambas as tags localmente (veja acima) e executando:```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 adicionado à lista de arquivos embutidos```diffprivate val HARDCODED_SPARK_FILE_LISTS = Seq( SPARK_JARS, SPARK_FILES, SPARK_ARCHIVES, SPARK_PY_FILES,
**Impacto da entrada ausente na v0.8.0:**
Quando um usuário submete uma sessão com `conf: {"spark.archives": "file:///etc/passwd"}`, o Livy
0.8.0 nunca chama `resolveURIs()` nesse valor e nunca o verifica contra
`livy.file.local-dir-whitelist`. O caminho é encaminhado ao Spark sem validação.
---
### Correção 2 — `Session.scala`: Normalização do caminho antes da verificação de whitelist```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.")
}
}
Impacto na v0.8.0:
A verificação de string bruta startsWith pode ser contornada com um payload de path traversal.
Exemplo: se `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** (contornado)
- v0.9.0: `Paths.get("/opt/safe-data/../../../etc/passwd").normalize` → `/etc/passwd`
`/etc/passwd`.startsWith(`/opt/safe-data`) → **false** (bloqueado)
## Resumo do Vetor de Ataque```
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
Todos os passos deste PoC foram executados e validados no seguinte sistema:
| Componente | Detalhe |
|---|---|
| SO do Host | Ubuntu 24.04.4 LTS (Noble Numbat) |
| Kernel | 6.17.0-14-generic x86_64 |
| Arquitetura | x86_64 |
| Memória Total | 15.49 GiB |
| Docker Engine | 28.2.2 |
| JDK do Host | OpenJDK 17.0.18 (usado somente pelo host — os containers usam eclipse-temurin:11-jdk-focal) |
| Imagem base do container | eclipse-temurin:11-jdk-focal (JDK 11, Ubuntu Focal) |
| Versão do Spark (ambas as imagens) | 3.1.3 com Hadoop 3.2 |
| Versão do Livy — imagem vulnerável | 0.8.0-incubating (build Scala 2.12) |
| Versão do Livy — imagem corrigida | 0.9.0-incubating (build Scala 2.12) |
. ├── 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
---
## Prova de Conceito