{"record":{"id":"07e7395e70834f57","repo":"unclecode/crawl4ai","slug":"refusing-to-write-download-through-symlink-downl","errorCode":null,"errorMessage":"Refusing to write download through symlink: {download_path}","messagePattern":"Refusing to write download through symlink: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"crawl4ai/async_crawler_strategy.py","lineNumber":1481,"sourceCode":"        2. Get the download path.\n        3. Log the download.\n        4. Start the download.\n        5. Save the downloaded file.\n        6. Log the completion.\n\n        Args:\n            download (Download): The Playwright download object\n\n        Returns:\n            None\n        \"\"\"\n        try:\n            suggested_filename = download.suggested_filename\n            download_path = _safe_download_filepath(self.browser_config.downloads_path, suggested_filename)\n            # Playwright's save_as performs the write itself (no O_NOFOLLOW\n            # hook), so reject a symlink planted at the target before writing.\n            if os.path.islink(download_path):\n                raise ValueError(f\"Refusing to write download through symlink: {download_path}\")\n\n            self.logger.info(\n                message=\"Downloading {filename} to {path}\",\n                tag=\"FETCH\",\n                params={\"filename\": suggested_filename, \"path\": download_path},\n            )\n\n            start_time = time.perf_counter()\n            await download.save_as(download_path)\n            end_time = time.perf_counter()\n            self._downloaded_files.append(download_path)\n\n            self.logger.success(\n                message=\"Downloaded {filename} successfully\",\n                tag=\"COMPLETE\",\n                params={\n                    \"filename\": suggested_filename,\n                    \"path\": download_path,","sourceCodeStart":1463,"sourceCodeEnd":1499,"githubUrl":"https://github.com/unclecode/crawl4ai/blob/7e801521428ee12509994d39151006f64055ebe3/crawl4ai/async_crawler_strategy.py#L1463-L1499","documentation":"A security guard in the download handler: before Playwright's download.save_as() writes the file, the code checks whether the resolved target path is a symlink and refuses to write through it. This prevents an attacker-controlled downloads directory (or a race-planted symlink) from redirecting the write to an arbitrary file outside the downloads root (symlink attack / TOCTOU mitigation).","triggerScenarios":"A file already exists at <downloads_path>/<suggested_filename> and is a symlink (possibly planted by another process or a previous malicious download). The server-controlled suggested_filename normally cannot itself contain '/', so the symlink must pre-exist at the target location.","commonSituations":"Shared or world-writable downloads directories where other users/processes created symlinks; leftover symlinks from prior runs or from extracting untrusted archives into the downloads dir; security scanners testing the crawler's download path.","solutions":["Inspect the offending path (ls -l) and remove or replace the symlink with a regular file/dir.","Use a dedicated, non-shared downloads directory with restrictive permissions (0700) that only the crawler writes to.","Clear or rotate the downloads directory between runs if it may be contaminated.","Treat repeated occurrences as a potential local attack or compromised sibling process and audit the directory's provenance."],"exampleFix":"// before\n# symlink exists: downloads/report.pdf -> /etc/cron.d/evil\n# crawler download raises ValueError: Refusing to write download through symlink\n\n// after\nimport os\np = os.path.join(downloads_path, suggested_name)\nif os.path.islink(p):\n    os.unlink(p)  # remove attacker-planted symlink\nawait crawler.arun(url, config)","handlingStrategy":"validation","validationCode":"import os\n\ndef download_target_safe(downloads_path: str, name: str) -> bool:\n    p = os.path.join(os.path.realpath(downloads_path), os.path.basename(name))\n    return not os.path.islink(p)","typeGuard":null,"tryCatchPattern":"try:\n    await crawler.arun(url, config=cfg)\nexcept ValueError as e:\n    if \"symlink\" in str(e):\n        os.unlink(extract_path_from_message(str(e)))  # remove and optionally retry","preventionTips":["Use a dedicated 0700 downloads directory","Clear symlinks from the downloads dir before runs","Never extract untrusted archives into the downloads dir"],"tags":["security","symlink","downloads","filesystem"],"backgroundTag":null,"analyzedSha":"7e801521428ee12509994d39151006f64055ebe3","analyzedAt":"2026-08-14T20:46:20.673Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}