
Um POC para a vulnerabilidade de bypass de lista de permissões de path traversal do 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. → Aviso Legal Completo
| Campo | Detalhe |
|---|---|
| CVE ID | CVE-2025-66249 |
| Gravidade | Importante (CVSS N/A — avaliação NVD pendente em 2026-03-15) |
| Afetados | Apache Livy 0.3.0-incubating até 0.8.0-incubating — apenas quando livy.file.local-dir-whitelist é definido com um valor não padrão |
| Corrigido em | Apache Livy 0.9.0-incubating |
| CWE | CWE-22: Limitação Incorreta de um Nome de Caminho a um Diretório Restrito ('Path Traversal') |
| Divulgado | 2026-03-12 (OSS-Sec) / 2026-03-13 (NVD) |
| Relator | Hiroki Egawa (descobridor) |
Um usuário autenticado com acesso à interface REST ou JDBC do Livy pode enviar uma sessão Spark ou um job em lote com um valor de configuração de caminho de arquivo manipulado que escapa à lista de permissões de diretório.
Causa raiz — Bypass de path traversal na verificação da lista de permissões (Session.scala)
Quando livy.file.local-dir-whitelist está configurado, o Livy 0.8.0 valida os
caminhos submetidos chamando String.startsWith() do Java no caminho bruto,
não normalizado. Essa verificação pode ser contornada usando sequências de
traversal ../:
/opt/safe-data/../sensitive/secret.txt
A string bruta começa com /opt/safe-data, então a verificação passa — mas o
caminho resolve para /opt/sensitive/secret.txt, que está completamente fora
do diretório permitido.
Condição de ativação: A vulnerabilidade só pode ser explorada quando
livy.file.local-dir-whitelist está definido com um valor não padrão (não vazio).
Se a lista de permissões estiver vazia (o padrão), a validação de caminho é
ignorada completamente e o problema não é acionado.
Impacto: Um atacante que envia uma sessão via API REST do Livy pode referenciar arquivos locais arbitrários no servidor Livy. Em um cluster de análise compartilhado, isso se traduz em potencial exposição de credenciais, chaves, arquivos de configuração ou qualquer dado legível pelo usuário do processo Livy.
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 do Apache Livy no GitHub usando os seguintes comandos exatos:
Repositório: https://github.com/apache/incubator-livy
# Versão vulnerável — clonada em ./livy-0.8.0/
git clone --depth=1 --branch v0.8.0-incubating \
https://github.com/apache/incubator-livy \
livy-0.8.0
# Versão corrigida — clonada em ./livy-0.9.0/
git clone --depth=1 --branch v0.9.0-incubating \
https://github.com/apache/incubator-livy \
livy-0.9.0
| Versão | Tag | Commit resolvido | Caminho local |
|---|---|---|---|
| 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() antes da verificação da lista de permissões 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.")
}
Impacto no v0.8.0:
A verificação startsWith da string bruta pode ser contornada com um payload de path traversal.
Exemplo: se 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 (contornado)Paths.get("/opt/safe-data/../sensitive/secret.txt").normalize → /opt/sensitive/secret.txt
/opt/sensitive/secret.txt.startsWith(/opt/safe-data) → false (bloqueado)Os diffs foram produzidos clonando ambas as tags localmente (veja acima) e executando:
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
Atacante (usuário REST/JDBC autenticado)
│
▼
POST /sessions
{
"conf": {
"spark.jars": "file:///opt/safe-data/../sensitive/secret.txt"
← caminho começa com prefixo permitido — String.startsWith() passa
← mas resolve FORA do diretório via traversal ../
}
}
│
▼
Livy 0.8.0 — verificação da lista de permissões contornada (startsWith bruto, sem normalização)
│
▼
Spark lê o arquivo e o distribui aos executores
│
▼
Atacante recupera o conteúdo do arquivo via saída do job / logs
Todas as etapas deste PoC foram executadas e validadas no seguinte sistema:
| Componente | Detalhe |
|---|---|
| SO do Hospedeiro | Ubuntu 24.04.4 LTS (Noble Numbat) |
| Kernel | 6.17.0-14-generic x86_64 |
| Arquitetura | x86_64 |
| Memória Total | 15 GiB |
| Docker Engine | 28.2.2 |
| JDK do Hospedeiro | OpenJDK 17.0.18 (usado apenas pelo hospedeiro — 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 imagens) | 3.1.3 com Hadoop 3.2 |
| Versão do Livy — imagem vulnerável | 0.8.0-incubating |
| Versão do Livy — imagem corrigida | 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/ → imagem: cve-2025-66249-vulnerable (Livy 0.8.0 + Spark 3.1.3)
docker/fixed/ → imagem: cve-2025-66249-fixed (Livy 0.9.0 + Spark 3.1.3)
test/validate.sh → script único, executado sem alterações em ambos os ambientes