GHSA-2JQ4-Q6VV-4CP3
Vulnerability from github – Published: 2026-06-18 17:25 – Updated: 2026-06-18 17:25Summary
When the crawler saves a downloaded file, the destination filename was taken from attacker-influenced input and joined to the downloads directory with no confinement. A filename containing an absolute path (e.g. /etc/cron.d/evil) or ../ traversal escaped the downloads directory, giving an arbitrary file write with attacker-controlled contents. Because the written bytes are attacker-controlled, this escalates to remote code execution (overwriting a shell rc-file, ~/.ssh/authorized_keys, a cron entry, or a Python module on the import path).
Affected paths
Two download sinks in crawl4ai/async_crawler_strategy.py:
- HTTP crawler (AsyncHTTPCrawlerStrategy): the filename is parsed from the response Content-Disposition header by _extract_filename() and written via aiofiles.open(filepath, 'wb'). Reachable directly via the SDK, and via the unauthenticated Docker /crawl endpoint when an HTTPCrawlerConfig is supplied.
- Browser crawler (AsyncPlaywrightCrawlerStrategy): the download's suggested_filename (controllable by the visited page) is joined to downloads_path and written via download.save_as().
The HTTP-strategy sink is reachable pre-auth on the default Docker deployment; both are reachable for SDK users simply by crawling an attacker-controlled URL. The default Playwright crawl path that does not trigger a download is unaffected.
Impact
Arbitrary file write with attacker-controlled content as the user running the crawler, escalating to remote code execution.
Fix
Both sinks now resolve the destination through a single hardened helper (_safe_download_filepath) that reduces the attacker-influenced name to a bare basename (dropping absolute paths and .. components) and re-checks, via realpath, that the resolved path stays inside the downloads root (defeating symlink/TOCTOU escapes). A traversal attempt is rejected; normal downloads are unchanged.
Workarounds
- Upgrade to the patched version (0.9.0).
- Run the crawler as an unprivileged user with a dedicated, isolated downloads directory on a volume with no sensitive paths writable.
- Enable authentication (
CRAWL4AI_API_TOKEN) on the Docker server.
Credits
Y4tacker - reported the Content-Disposition path traversal in the HTTP crawler with a clear PoC and a basename + realpath-containment fix recommendation.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.8.9"
},
"package": {
"ecosystem": "PyPI",
"name": "crawl4ai"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.9.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-22",
"CWE-59"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-18T17:25:13Z",
"nvd_published_at": null,
"severity": "CRITICAL"
},
"details": "### Summary\n\nWhen the crawler saves a downloaded file, the destination filename was taken from attacker-influenced input and joined to the downloads directory with no confinement. A filename containing an absolute path (e.g. `/etc/cron.d/evil`) or `../` traversal escaped the downloads directory, giving an arbitrary file write with attacker-controlled contents. Because the written bytes are attacker-controlled, this escalates to remote code execution (overwriting a shell rc-file, `~/.ssh/authorized_keys`, a cron entry, or a Python module on the import path).\n\n### Affected paths\n\nTwo download sinks in `crawl4ai/async_crawler_strategy.py`:\n- HTTP crawler (`AsyncHTTPCrawlerStrategy`): the filename is parsed from the response `Content-Disposition` header by `_extract_filename()` and written via `aiofiles.open(filepath, \u0027wb\u0027)`. Reachable directly via the SDK, and via the unauthenticated Docker `/crawl` endpoint when an `HTTPCrawlerConfig` is supplied.\n- Browser crawler (`AsyncPlaywrightCrawlerStrategy`): the download\u0027s `suggested_filename` (controllable by the visited page) is joined to `downloads_path` and written via `download.save_as()`.\n\nThe HTTP-strategy sink is reachable pre-auth on the default Docker deployment; both are reachable for SDK users simply by crawling an attacker-controlled URL. The default Playwright crawl path that does not trigger a download is unaffected.\n\n### Impact\n\nArbitrary file write with attacker-controlled content as the user running the crawler, escalating to remote code execution.\n\n### Fix\n\nBoth sinks now resolve the destination through a single hardened helper (`_safe_download_filepath`) that reduces the attacker-influenced name to a bare basename (dropping absolute paths and `..` components) and re-checks, via `realpath`, that the resolved path stays inside the downloads root (defeating symlink/TOCTOU escapes). A traversal attempt is rejected; normal downloads are unchanged.\n\n### Workarounds\n\n- Upgrade to the patched version (0.9.0).\n- Run the crawler as an unprivileged user with a dedicated, isolated downloads directory on a volume with no sensitive paths writable.\n- Enable authentication (`CRAWL4AI_API_TOKEN`) on the Docker server.\n\n### Credits\n\nY4tacker - reported the Content-Disposition path traversal in the HTTP crawler with a clear PoC and a basename + realpath-containment fix recommendation.",
"id": "GHSA-2jq4-q6vv-4cp3",
"modified": "2026-06-18T17:25:13Z",
"published": "2026-06-18T17:25:13Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/unclecode/crawl4ai/security/advisories/GHSA-2jq4-q6vv-4cp3"
},
{
"type": "PACKAGE",
"url": "https://github.com/unclecode/crawl4ai"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "Crawl4AI: Arbitrary file write (path traversal) in crawler downloads can lead to RCE"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.