Crawl4AI JsonCssExtractionStrategy AST Sandbox Escape → Unauthenticated RCE (CVE-2026-53753)
Published: 2026-07-27 • Researcher: Caio Fabrício (BiiTts) — corroborated by 0xEnc0der
- Severity
- Critical
- CVE
- CVE-2026-53753 (GHSA-qxjp-w3pj-48m7)
- Category
- web
- Affected product
- Crawl4AI — open-source LLM-friendly web crawler/scraper, Docker API server
- Affected versions
- <= 0.8.6 (fixed in 0.8.7)
- Disclosed
- 2026-07-27
- Patch status
- Patched
References
Archive entry
intelseclab/poc-archiveOn this page
Metadata
| Field | Value |
|---|---|
| Date Added | 2026-07-27 |
| Last Updated | 2026-07-27 |
| Author / Researcher | Caio Fabrício (BiiTts) — corroborated by 0xEnc0der |
| CVE / Advisory | CVE-2026-53753 (GHSA-qxjp-w3pj-48m7) |
| Category | web |
| Severity | Critical |
| CVSS Score | 9.8 (CVSS 3.1, AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) |
| Status | Weaponized — full end-to-end command execution reproduced against the official unclecode/crawl4ai:0.8.6 image |
| Tags | crawl4ai, sandbox-escape, rce, python, ast-bypass, unauthenticated, llm-tooling, ai-security |
| Related | N/A |
Affected Target
| Field | Value |
|---|---|
| Software / System | Crawl4AI — open-source LLM-friendly web crawler/scraper, Docker API server |
| Versions Affected | <= 0.8.6 (fixed in 0.8.7) |
| Language / Platform | Python 3 (crawl4ai’s extraction_strategy.py), served via the project’s Docker API image |
| Authentication Required | No — the shipped Docker image config has security.jwt_enabled: false, so the /crawl token dependency is a no-op |
| Network Access Required | Yes — direct reachability to the Crawl4AI API port (default 11235) |
Summary
Crawl4AI’s JsonCssExtractionStrategy supports “computed fields” — small Python expressions evaluated against each extracted item via _safe_eval_expression(). That function tries to sandbox the expression with an AST allow-list (rejecting only names/attributes/calls that start with _, plus import statements) and a stripped-down __builtins__ dict. The allow-list is name-prefix-based and never inspects ast.Subscript keys, ast.Lambda, ast.GeneratorExp, or ast.NamedExpr nodes. An attacker-supplied expression can therefore walk a running generator’s frame chain (gi_frame → f_back × 3 → f_builtins) — none of which start with _ — to reach the real, unrestricted builtins module of the calling frame, then pull __import__ out of it via a dict subscript (['__import__'], never checked as an attribute). From there __import__('os').popen(cmd).read() executes an arbitrary shell command and returns its stdout in-band as the computed field’s value. Because the default Docker deployment ships with JWT auth disabled, this is reachable by any unauthenticated network client that can reach POST /crawl, yielding full remote code execution as the container’s service user.
Vulnerability Details
Root Cause
crawl4ai/extraction_strategy.py (v0.8.6) implements _safe_eval_expression() as a deny-by-prefix AST validator:
| |
The validator only inspects four node shapes (imports, Attribute.attr, Call on a Name, Call on an Attribute) and only rejects identifiers that literally start with _. Three gaps combine into a full sandbox escape:
- Frame-introspection attributes
gi_frame,f_back,f_builtinsdo not start with_, so the entire Python frame chain is walkable. dict['__import__']is anast.Subscript, never anast.Attribute— the validator never inspects subscript keys, so the dunder string'__import__'slips through as plain data.- A frame’s
f_backlink is only populated while that frame is executing. Driving a self-referencing generator (bound via the walrus operator inside alambda, so the binding is a closure cell) withlist(g)— itself a permitted call — keeps the generator’s frame live long enough to readf_backthree times up to the_safe_eval_expressioncaller frame, whosef_builtinsis the real, unrestricted builtins module (as opposed to the sandboxed_SAFE_EVAL_BUILTINSused inside theeval()’d code).
Chaining these: g.gi_frame.f_back.f_back.f_back.f_builtins['__import__']('os').popen(cmd).read() reaches os.popen and returns command stdout as the computed field’s value, none of it ever tripping the _-prefix check.
Attack Vector
Unauthenticated POST /crawl to the Crawl4AI Docker API server with a crawler_config whose extraction_strategy is a JsonCssExtractionStrategy schema containing a computed-type field with the malicious expression. Using a raw://<html> pseudo-URL lets the request supply its own HTML inline (matching a trivial baseSelector, e.g. div), so no outbound fetch is required — the whole exploit is a single self-contained HTTP request. No authentication is needed because the shipped default config has jwt_enabled: false, making the /crawl auth dependency a no-op.
Impact
Unauthenticated remote code execution as the Crawl4AI container’s service user (appuser in the official image), with command output returned in-band in the JSON response — no OAST/blind-exfiltration channel required for initial confirmation. Full compromise of the Crawl4AI host and a pivot point into any internal network/resources it can reach.
Environment / Lab Setup
OS: Any Docker host (Linux/macOS/WSL)
Target: unclecode/crawl4ai:0.8.6 (official Docker image, vulnerable default config)
Attacker: Any host with Python 3 (standard library only, no extra deps)
Tools: exploit.py (this folder)Setup Steps
| |
Proof of Concept
See
exploit.py,ANALYSIS.md, andlab/docker-compose.yml(all real, unmodified) plusupstream-README.mdin this folder — mirrored from BiiTts/CVE-2026-53753-Crawl4AI-RCE. Verified before ingestion: read the full exploit script, the docker-compose lab definition, and the accompanying line-by-lineANALYSIS.mdwalkthrough of the AST validator, the payload’s node-by-node treatment, the runtime frame stack, the request data-flow, and the 0.8.6→0.8.7 patch diff. The exploit uses only the Python standard library, targets a real, documented sink in Crawl4AI’s ownextraction_strategy.py, and reproduces end-to-end against the officialunclecode/crawl4ai:0.8.6image with no obfuscation, no unrelated network calls, and no destructive default behavior.
Step-by-Step Reproduction
Stand up the vulnerable target — bring up the official
crawl4ai:0.8.6image (unauthenticated by default):Shell script1docker compose -f lab/docker-compose.yml up -dInspect the crafted payload without sending it:
Shell script1python3 exploit.py http://127.0.0.1:11235 -c "id" --print-payloadFire the exploit — command stdout is returned in-band in the
/crawlJSON response:Shell script1python3 exploit.py http://127.0.0.1:11235 -c "id; uname -a; cat /etc/os-release | head -1"
Exploit Code
See
exploit.pyin this folder for the full, unmodified PoC.
| |
Expected Output
[*] POST http://127.0.0.1:11235/crawl (cmd: 'id; uname -a; ...', no auth)
[*] HTTP 200
{"success":true,"results":[{ ... "extracted_content":"[
{
\"out\": [
\"uid=999(appuser) gid=999(appuser) groups=999(appuser)
appuser
Linux ... x86_64 GNU/Linux
PRETTY_NAME=\"Debian GNU/Linux 12 (bookworm)\"
\"
]
}
]" ...The out field is live OS state read directly from the container (id, uname, /etc/os-release) rather than an echo of the request — uid=999(appuser) confirms code execution inside the Crawl4AI host’s service account.
Screenshots / Evidence
N/A — reproduction is via HTTP request/response shown above; no screenshots included in the upstream repo.
Detection & Indicators of Compromise
SIEM / IDS Rule (example):
alert http any any -> any any (msg:"Possible Crawl4AI CVE-2026-53753 sandbox escape attempt"; content:"gi_frame"; http_client_body; content:"f_builtins"; http_client_body; sid:9000002;)Remediation
| Action | Detail |
|---|---|
| Patch | Upgrade to Crawl4AI >= 0.8.7. The fix does not attempt to harden the AST validator — it removes the eval path entirely: the expression computed-field key now unconditionally raises ValueError("Computed field 'expression' is disabled for security (eval on untrusted input). Use 'function' key with a Python callable instead."), and _safe_eval_expression no longer exists. |
| Workaround | Enable JWT authentication (security.jwt_enabled: true plus api_token) and never expose the Crawl4AI API to untrusted networks. |
| Config Hardening | Restrict /crawl and /crawl/stream reachability to trusted, authenticated callers only; treat any computed-field expression key in incoming requests as suspicious pending the upgrade. |
References
Notes
A second, independently-authored repository (0xEnc0der/CVE-2026-53753) corroborates the same vulnerability mechanism (the _safe_eval_expression AST-allow-list bypass via frame introspection), which increases confidence that this is a genuine, reproducible flaw in Crawl4AI’s computed-field evaluator rather than a one-off or fabricated claim.
The primary PoC (BiiTts) is unusually well-documented for this archive: beyond exploit.py, it ships a full ANALYSIS.md doing a node-by-node ast.walk() table (every AST node in the payload mapped to the exact validator check it evades), a runtime frame-stack diagram explaining why the generator must be self-referencing and actively driven for f_back to be populated, the full request data-flow from POST /crawl down to the _safe_eval_expression sink, and the exact 0.8.6→0.8.7 patch diff — consistent with genuine, tested reverse engineering rather than a templated or guessed exploit. The lab/docker-compose.yml pins the exact vulnerable image (unclecode/crawl4ai:0.8.6) used for verification.
| |