{"record":{"id":"1016cd00d0f75d19","repo":"ComposioHQ/composio","slug":"unsafe-path-component-e","errorCode":null,"errorMessage":"Unsafe path component: {e}","messagePattern":"Unsafe path component: (.+?)","errorType":"exception","errorClass":"ErrorDownloadingFile","httpStatus":null,"severity":"error","filePath":"python/composio/core/models/_files.py","lineNumber":714,"sourceCode":"            input, provided it was produced by :func:`secure_join`.\n        :param root: Trusted containment anchor, configured locally and never\n            derived from an API response. Required rather than defaulted:\n            checking containment against a directory that untrusted input has\n            already relocated is not a check at all, and ``outdir`` may be\n            exactly such a directory. Callers must name the anchor explicitly.\n        :param max_size: Maximum number of bytes to write to disk. ``s3url`` is\n            an API-response field, so the body behind it is untrusted: the\n            streamed byte count is authoritative because ``Content-Length`` can\n            be absent or dishonest.\n        \"\"\"\n        # SEC-316: `self.name` also comes from the (potentially compromised or\n        # MITM'd) API response. Collapsed to a bare filename and checked against\n        # `root` — not `outdir` — so a name like `output_evil/foo`\n        # (sibling-prefix attack) is rejected too.\n        try:\n            outfile = secure_basename_join(outdir, self.name, root=root)\n        except UnsafePathComponentError as e:\n            raise ErrorDownloadingFile(str(e)) from e\n        try:\n            response = safe_get(\n                self.s3url,\n                stream=True,\n                timeout=(_CONNECT_TIMEOUT, _READ_TIMEOUT),\n            )\n        except requests.exceptions.RequestException as e:\n            raise ErrorDownloadingFile(\n                \"Error downloading file: \"\n                f\"{_sanitize_url_for_logging(self.s3url)}. Error: {type(e).__name__}\"\n            ) from e\n        if response.status_code != 200:\n            response.close()\n            raise ErrorDownloadingFile(\n                f\"Error downloading file: {_sanitize_url_for_logging(self.s3url)}\"\n            )\n\n        # Early abort for a self-declared oversized body. The header is only a","sourceCodeStart":696,"sourceCodeEnd":732,"githubUrl":"https://github.com/ComposioHQ/composio/blob/64b1b85502b1beeb2379e6c9e8bf1104504fa637/python/composio/core/models/_files.py#L696-L732","documentation":"When downloading a FileModel, the remote-supplied filename (self.name) is joined into the output directory via secure_basename_join, which rejects path traversal and unsafe components. A violation (../ sequences, absolute paths, sibling-prefix attacks like output_evil/foo) is re-raised as ErrorDownloadingFile.","triggerScenarios":"FileModel.download (or _download_file_value) where the API-provided file name contains slashes, .., or otherwise tries to escape the output root — usually a compromised/misbehaving backend response, since the name comes from the server.","commonSituations":"Rare in practice; indicates a tampered or buggy API response, a filename containing unexpected separators (e.g. 'reports/2024/q1.pdf' stored verbatim), or a middlebox/CDN altering responses.","solutions":["If you control the naming, ensure the uploaded filename is a bare filename (no directories, no ..).","Treat this defensively: it protects you from path traversal — do not try to bypass it; report it if the name looks legitimately benign (slash-containing names) so the SDK/backend sanitizes.","As a workaround, download the raw bytes yourself from s3url and choose your own filename."],"exampleFix":"# before\nawait model.download(outdir=Path('/tmp/out'))  # model.name = 'a/../evil.txt' -> raises\n# after: fetch content yourself with a safe name\nimport requests\nsafe = Path('/tmp/out') / 'downloaded.bin'\nsafe.write_bytes(requests.get(model.s3url, timeout=60).content)","handlingStrategy":"try-catch","validationCode":"from pathlib import PurePosixPath\n\ndef safe_name(name: str) -> bool:\n    return bool(name) and '/' not in name and '\\\\' not in name and name not in {'.', '..'}","typeGuard":"def is_safe_basename(name: str) -> bool:\n    p = PurePosixPath(name)\n    return p.name == name and name not in {'.', '..'} and not name.startswith('/')","tryCatchPattern":"from composio.core.models._files import ErrorDownloadingFile\n\ntry:\n    path = model.download(outdir=outdir)\nexcept ErrorDownloadingFile as e:\n    if 'Unsafe path' in str(e) or 'component' in str(e):\n        import requests\n        safe = outdir / 'downloaded.bin'\n        safe.write_bytes(requests.get(model.s3url, timeout=120).content)\n        path = safe\n    else:\n        raise","preventionTips":["Upload files with plain basenames (no directories, no ..).","Treat this error as a security signal; investigate unexpected sources.","Sanitize user-supplied filenames before creating remote files."],"tags":["python","security","path-traversal","download","filename"],"backgroundTag":"path-traversal-rejected","analyzedSha":"64b1b85502b1beeb2379e6c9e8bf1104504fa637","analyzedAt":"2026-08-28T15:39:33.623Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}