~/f4n6 $ grep -r "Crawl4AI: Multiple Docker API Vulnerabilities - File Write, SSRF, Auth Bypass, XSS, JS Execution" ./investigations/ --include="*.md"

Crawl4AI: Multiple Docker API Vulnerabilities - File Write, SSRF, Auth Bypass, XSS, JS Execution

Jeff Davies 24 Sep 2026 9 min read

1. Executive summary

Eleven vulnerabilities across five GitHub Security Advisories affect the Crawl4AI Docker API server, an open-source crawling and LLM-extraction service that ships unauthenticated by default. The highest-scored issue is a hardcoded JWT signing key default of "mysecret" in the public source (CVSS 9.8 as published by the advisory), which lets anyone forge valid authentication tokens, followed by an arbitrary file write through the /screenshot and /pdf output_path parameter (CVSS 9.1) that reaches code execution where the container runtime user can write to executable or cron locations. Four separate SSRF paths reach cloud instance metadata at 169.254.169.254, including a proxy-configuration bypass that passes crawl-URL validation and IPv6 transition-form encodings that defeat a naive blocklist. No CISA KEV entry and no EPSS score were available for this item, and every CVSS value below is as published by the GitHub advisory rather than independently re-scored. Any client running the Crawl4AI Docker image on a network-reachable host should treat this as an emergency upgrade to 0.8.9.

2. Regulatory framing

No specific DORA/NIS2 article is directly engaged by this item.

This is a pre-exploitation vulnerability disclosure. There is no confirmed incident, no confirmed data loss, and no evidence in the source material that any of these flaws has been exploited in the wild. The DORA incident articles (Art. 17, Art. 18, Art. 19) and NIS2 Art. 23 are triggered by an actual ICT-related incident, not by the availability of a patch. DORA Art. 28 to Art. 30 address contractual arrangements with ICT third-party service providers; Crawl4AI is an open-source component distributed without a support contract, so those provisions do not attach on the facts here. If a client establishes that the metadata SSRF or the file write was exercised against its own deployment, that finding changes the position and the incident-reporting clock starts from the moment of detection.

3. Technical analysis & attack chain

The Crawl4AI Docker API server is unauthenticated by default (jwt_enabled: false). Unless CRAWL4AI_API_TOKEN is set, every endpoint below is reachable without credentials. The patched version is 0.8.9: 0.8.8 closed the LLM credential-exfiltration issues and 0.8.9 closed the proxy SSRF bypass.

Triage table

Ordered by published CVSS, since no KEV entry and no EPSS score were available for any entry. Where the source gives no CVSS, the cell reads n/a.

CVE / advisory component CVSS KEV / EPSS why it matters
CVE-2026-56266 (GHSA-365w-hqf6-vxfg) JWT signing key 9.8 no KEV entry; no EPSS available default key "mysecret" is in the public source; any token can be forged
CVE-2026-56266 (GHSA-365w-hqf6-vxfg) /screenshot, /pdf output_path 9.1 no KEV entry; no EPSS available arbitrary filesystem write; overwrites server files or writes anywhere the appuser can write
GHSA-7cx2-g3h9-382p /screenshot, /pdf output_path (symlink / TOCTOU) n/a no KEV entry; no EPSS available the 0.8.7 containment check was string-only and did not resolve symlinks; a symlinked path component escapes the output directory
CVE-2026-56266 (GHSA-365w-hqf6-vxfg) /crawl/job, /llm/job webhook URL 8.6 no KEV entry; no EPSS available webhook URLs accept RFC 1918, loopback, link-local and metadata addresses
CVE-2026-56266 (GHSA-365w-hqf6-vxfg) /crawl, /crawl/stream, /md, /llm 8.6 no KEV entry; no EPSS available primary crawl entry points fetch arbitrary URLs; IPv6-mapped IPv4 evades a literal-hostname check
GHSA-6qhc-x826-342c browser_config.proxy_config.server and related proxy fields n/a no KEV entry; no EPSS available a benign crawl URL passes validation while the proxy routes Chromium to an internal address
GHSA-4qqr-vv2q-cmr5 validate_webhook_url / validate_url_destination n/a no KEV entry; no EPSS available NAT64, 6to4, IPv4-mapped, IPv4-compatible and the IPv6 unspecified address all bypassed the CIDR blocklist
CVE-2026-56266 (GHSA-365w-hqf6-vxfg) /execute_js 8.1 no KEV entry; no EPSS available arbitrary JavaScript in the server browser with --disable-web-security enabled
GHSA-f989-c77f-r2cq, GHSA-mw9f-q68p-364x /md, /llm, /llm/job base_url and env: token resolution n/a no KEV entry; no EPSS available exfiltrates LLM provider keys and server secrets including SECRET_KEY
CVE-2026-56266 (GHSA-365w-hqf6-vxfg) /monitor/* 6.5 no KEV entry; no EPSS available monitor router mounted without token_dep; destructive endpoints such as /monitor/actions/cleanup are open
CVE-2026-56266 (GHSA-365w-hqf6-vxfg) monitor dashboard 6.1 no KEV entry; no EPSS available stored XSS via crafted crawl URLs rendered through innerHTML
GHSA-7cx2-g3h9-382p logging, outbound webhook headers n/a no KEV entry; no EPSS available CRLF log injection (CWE-117) and webhook request-header injection (CWE-93/CWE-113)

Confirmed exploitation path

  1. Reach the API. The Docker API server is unauthenticated by default. If CRAWL4AI_API_TOKEN is unset, no credential is required for any step that follows. If JWT is enabled but SECRET_KEY was left at the default, the attacker signs a token with "mysecret" and proceeds authenticated.
  2. Retrieve cloud credentials by SSRF. POST /crawl with a URL pointing at 169.254.169.254 makes the server fetch instance metadata. The advisory names AWS IMDSv1 as the target for IAM credential tokens. The same applies to /crawl/stream, /md and /llm, and to webhook URLs supplied to /crawl/job and /llm/job.
  3. Defeat the blocklist where one is present. Encode the internal address in an IPv6 transition form. The advisory lists ::, 64:ff9b::a9fe:a9fe (NAT64), 2002:a9fe:a9fe:: (6to4), ::ffff:169.254.169.254 (IPv4-mapped) and ::a9fe:a9fe (IPv4-compatible) as forms that resolved to blocked ranges but were not caught. The pre-fix error message also echoed the resolved internal IP, giving the attacker a DNS oracle.
  4. Bypass the crawl-URL check entirely with a proxy. Send /crawl with a benign, validation-passing URL and a proxy_config.server pointing at an internal address. Chromium routes all requests through that proxy. For plain-HTTP targets the proxy receives the full request and returns any content, which the API returns verbatim in results[0].html, cleaned_html or markdown. The same unchecked surface covers browser_config.proxy (deprecated), crawler_config.proxy_config.server, and the --proxy-server, --proxy-pac-url, --proxy-bypass-list and --host-resolver-rules flags in browser_config.extra_args.
  5. Exfiltrate LLM provider keys and server secrets. POST /md, /llm or /llm/job with base_url set to an attacker-controlled host. The server attaches its configured provider API key to the outbound request, so the key arrives at the attacker endpoint. Separately, LLMConfig(api_token="env:NAME") resolves NAME from the server environment via os.getenv; because request bodies deserialise into LLMConfig, an attacker sets api_token="env:SECRET_KEY" or env:REDIS_PASSWORD and, paired with the attacker base_url, reads that secret out. Reading SECRET_KEY yields token forgery.
  6. Write files, and potentially execute code. POST /screenshot or POST /pdf with an output_path outside the intended directory. The pre-fix code accepted arbitrary filesystem paths with no validation, so the attacker overwrites server files (denial of service) or writes anywhere the appuser can write. The symlink/TOCTOU variant is the sharper edge: the 0.8.7 validate_output_path check was string-only and did not resolve symlinks, so a symlinked path component inside the output directory redirected the write outside it, and the final open() followed the symlink. On a deployment where the runtime user can write executable or cron locations, that is an arbitrary-write to code-execution primitive.
  7. Execute JavaScript in the server browser. POST /execute_js runs arbitrary JavaScript with --disable-web-security set in the default browser arguments, combining script execution with the SSRF capability above.
  8. Reach the monitor plane without credentials. The monitor router was mounted without the token_dep dependency, so every monitoring endpoint, including destructive ones such as /monitor/actions/cleanup, was reachable unauthenticated. The WebSocket endpoint /monitor/ws had no token check either. Crafted crawl URLs stored and rendered through innerHTML in the monitor dashboard give stored XSS against an operator viewing that dashboard.

Fix detail and discrepancies

The primary advisory describes the file-write fix as adding validate_output_path() to restrict writes to CRAWL4AI_OUTPUT_DIR (/tmp/crawl4ai-outputs by default) plus a Pydantic field_validator rejecting .. traversal sequences. GHSA-7cx2-g3h9-382p describes the same control differently: it states the 0.8.7 check was string-only and did not resolve symlinks, and that the fix now resolves the real path of the parent, re-checks containment, and writes with O_NOFOLLOW via write_output_file. Both descriptions cover the same control at different fix stages; the symlink-resolving behaviour is the one that matters for containment. A second discrepancy sits in the SSRF fix: the primary advisory says IPv6-mapped IPv4 addresses are normalised to IPv4 form before the blocklist check, while GHSA-4qqr-vv2q-cmr5 says the CIDR blocklist was replaced by a single not ip.is_global rule evaluated on the address and every embedded IPv4 transition form. Treat the is_global rule as the current behaviour.

The SSRF fixes apply at request entry, not only at fetch time, and raw:// URLs are skipped. Webhook validation runs at both job submission and send time with follow_redirects=False. /execute_js is disabled by default behind CRAWL4AI_EXECUTE_JS_ENABLED, and --disable-web-security was removed from the default browser arguments. The LLM endpoints now ignore a request-supplied base_url and derive the endpoint server-side from the provider name; the field is still accepted so no 4xx is returned. LLMConfig refuses env: resolution of protected names (names containing SECRET, PASSWORD or PRIVATE, prefixes CRAWL4AI* and AWS_SECRET*, and SECRET_KEY, REDIS_PASSWORD, TOKEN). Normal provider keys such as OPENAI_API_KEY are unaffected.

Confidence caveats

All of the above rests on the five GitHub Security Advisories listed in section 7. There is no independent vendor research, no telemetry and no exploitation reporting in the source material. The CVSS values are the advisory's own and have not been corroborated against NVD. The researcher credits in the primary advisory (Jeongbean Jeon, wulonchia, by111/August829, secsys_codex, Velayutham Selvaraj, IcySun and Yashon) are individual reporters, not tracked threat actors, and no MITRE ATT&CK group profile applies. Nothing in the source material indicates ransomware, extortion or data theft.

4. Mitigation & containment

P1, within 24 hours

  • Upgrade the Crawl4AI Docker image to 0.8.9. This is the floor: 0.8.8 closes the LLM credential-exfiltration issues and 0.8.9 closes the proxy SSRF bypass. Pin the image by digest rather than tag.
  • If an upgrade cannot be completed today, set CRAWL4AI_API_TOKEN to a long random value so the API is no longer unauthenticated, and set SECRET_KEY to a random value of at least 32 characters. Do not leave JWT enabled with the default key.
  • Cut the container's route to link-local address space. Block 169.254.0.0/16 at the egress firewall or the container network policy, and enforce IMDSv2 on any AWS instance hosting the service so a metadata fetch without a session token fails.
  • Stop exposing the Docker API beyond the host. Bind it to loopback or a dedicated internal network instead of 0.0.0.0, and remove any public ingress rule pointing at the API port.
  • Hunt for prior exploitation in API access logs: unauthenticated requests to /monitor/* or /monitor/ws, output_path values containing .. or absolute paths outside CRAWL4AI_OUTPUT_DIR, and crawl or webhook URLs resolving to 169.254.169.254, RFC 1918, loopback or link-local addresses.

P2, within 72 hours

  • Set CRAWL4AI_EXECUTE_JS_ENABLED=false explicitly rather than relying on the patched default.
  • Run the container with a read-only root filesystem and mount a dedicated, non-executable volume for CRAWL4AI_OUTPUT_DIR. This removes the code-execution edge from the file-write primitive even on an unpatched build.
  • Apply an egress allowlist to the container so it can reach only the sites and LLM providers it needs. This is the control that survives an unpatched SSRF.
  • Rotate every secret that was present in the container environment: LLM provider API keys, SECRET_KEY, REDIS_PASSWORD, and any AWS_SECRET* value. Rotate them after the upgrade, not before, or the new values are exposed by the same flaw.
  • Review the log pipeline for forged entries. The CRLF log injection issue let user-controlled URLs and errors embed CR/LF and fabricate additional log lines, so any log-based alerting that ran during the exposure window needs re-checking against raw source records.

P3, within 7 days

  • Inventory every Crawl4AI deployment, including development, CI and internal tooling instances. The unauthenticated default means a forgotten instance is as exposed as a production one.
  • Remove sensitive secrets from the container environment entirely. The env: resolution path exists because secrets sit alongside provider keys in the same environment.
  • Deploy the detection rules in section 6 and confirm they fire against a controlled test request before relying on them.

5. Indicators of compromise

No atomic indicators of compromise are available in the source material. There are no hashes, no domains, no IP addresses and no file paths belonging to an attacker. The sources describe observable behaviours only, and those are listed below.

Behavioural indicators

behaviour where to observe confidence
Requests to /monitor/*, including /monitor/actions/cleanup, with no authentication token API access logs, reverse proxy logs high, documented in GHSA-365w-hqf6-vxfg
WebSocket connection to /monitor/ws with no token check reverse proxy logs high, documented in GHSA-365w-hqf6-vxfg
output_path values containing .. or resolving outside CRAWL4AI_OUTPUT_DIR API access logs high, documented in GHSA-365w-hqf6-vxfg and GHSA-7cx2-g3h9-382p
Crawl or webhook URLs resolving to 169.254.169.254, RFC 1918, loopback or link-local addresses API access logs, egress firewall logs high, documented in GHSA-365w-hqf6-vxfg
URL fields containing IPv6 transition forms: ::ffff:, 64:ff9b::, 2002:a9fe, ::a9fe:a9fe, or the bare unspecified address :: API access logs high, documented in GHSA-4qqr-vv2q-cmr5
browser_config.proxy_config.server, browser_config.proxy or crawler_config.proxy_config.server set to an internal address, or --proxy-server, --proxy-pac-url, --proxy-bypass-list, --host-resolver-rules present in browser_config.extra_args API access logs high, documented in GHSA-6qhc-x826-342c
base_url in a /md, /llm or /llm/job request pointing at a host that is not the configured LLM provider API access logs high, documented in GHSA-f989-c77f-r2cq
api_token values of the form env:NAME, particularly env:SECRET_KEY or env:REDIS_PASSWORD API access logs high, documented in GHSA-f989-c77f-r2cq
Outbound HTTP from the container to an unexpected host carrying an Authorization header egress proxy logs medium, inferred from the documented base_url behaviour
CR or LF characters in logged URL or error fields producing additional log lines log ingestion pipeline medium, documented in GHSA-7cx2-g3h9-382p
Outbound webhook requests carrying attacker-supplied Host, Content-Length, Transfer-Encoding, Authorization or Cookie headers egress logs medium, documented in GHSA-7cx2-g3h9-382p

6. Detection

The YARA rule below hunts deployed Crawl


Read the original source →

Published via PulseTrace — Adverse Trace threat intelligence.

Post this to LinkedIn
Formatting is converted automatically — headings, bullets, a link back & hashtags. Paste straight in.
J
Jeff Davies