{"record":{"id":"75e2638da2a25038","repo":"ComposioHQ/composio","slug":"path-traversal-detected-label-name-r-resolves","errorCode":null,"errorMessage":"Path traversal detected: {label} {name!r} resolves to {candidate.resolve()}, which is outside {resolved_root}.","messagePattern":"Path traversal detected: (.+?) (.+?) resolves to (.+?), which is outside (.+?)\\.","errorType":"exception","errorClass":"UnsafePathComponentError","httpStatus":null,"severity":"critical","filePath":"python/composio/utils/safe_path.py","lineNumber":253,"sourceCode":") -> Path:\n    \"\"\"Join a single untrusted filename under ``base``, contained within ``root``.\n\n    The filename counterpart to :func:`secure_join`, which cannot be reused here\n    because it forbids ``.`` — correct for a slug, wrong for ``report.pdf``.\n\n    ``root`` defaults to ``base`` but is separate for the download path, where\n    ``base`` is a per-tool subdirectory that untrusted slugs helped build and\n    only the configured ``root`` above it is trusted. Anchoring on ``base``\n    there would check the result against a directory those slugs had moved.\n\n    :raises UnsafePathComponentError: when ``name`` is unsafe, or when the\n        result escapes ``root``.\n    \"\"\"\n    resolved_base = resolve_root(base)\n    resolved_root = resolved_base if root is None else resolve_root(root)\n    candidate = resolved_base / safe_basename(name, label=label)\n    if not is_inside_dir(candidate.resolve(), resolved_root):\n        raise UnsafePathComponentError(\n            f\"Path traversal detected: {label} {name!r} resolves to \"\n            f\"{candidate.resolve()}, which is outside {resolved_root}.\"\n        )\n    return candidate\n\n\ndef secure_join(root: t.Union[str, Path], *components: str) -> Path:\n    \"\"\"Join untrusted ``components`` beneath the trusted ``root``.\n\n    ``root`` is the sole anchor of trust and must not itself be derived from\n    untrusted input — that is the whole point. Each component is validated by\n    :func:`assert_safe_path_component`, then the joined result is resolved and\n    re-checked against the resolved root. The second check is belt-and-braces:\n    it catches a symlink inside ``root`` pointing outside it, which per-component\n    validation cannot see.\n\n    Performs no filesystem writes; the caller creates directories only after\n    this returns.","sourceCodeStart":235,"sourceCodeEnd":271,"githubUrl":"https://github.com/ComposioHQ/composio/blob/64b1b85502b1beeb2379e6c9e8bf1104504fa637/python/composio/utils/safe_path.py#L235-L271","documentation":"secure_basename_join detected that base joined with the sanitized basename resolves (via Path.resolve, following symlinks and ..) to a location outside the trusted root. This is the final containment check ensuring an untrusted filename cannot escape the configured directory even when the base itself is derived from untrusted input.","triggerScenarios":"secure_basename_join(base, name, root=root) where base is '~' expanded to the home directory or a symlinked path while root points elsewhere — the resolved candidate lands outside resolved_root. Used by the SDK's download/save helpers.","commonSituations":"Passing a user-controlled base (from an API field like '~/...' or a symlinked cache dir) instead of a constant root; tilde paths expanded before the call; root and base disagreeing after symlink resolution.","solutions":["Pass a constant, trusted root directory as both base and root (secure_basename_join(root, name, root=root))","Never derive base from untrusted input; anchor containment on a literal/module constant","Ensure root is the resolved form of the directory you intend to contain writes to"],"exampleFix":"# before\nsecure_basename_join(api_base_dir, filename, root=my_root)\n# after\nsecure_basename_join(my_root, filename, root=my_root)","handlingStrategy":"try-catch","validationCode":"from composio.utils.safe_path import resolve_root, is_inside_dir\nfrom pathlib import Path\ndef join_stays_inside(base, name, root):\n    cand = (resolve_root(base) / Path(name).name)\n    return is_inside_dir(cand.resolve(), resolve_root(root))","typeGuard":null,"tryCatchPattern":"from composio.exceptions import UnsafePathComponentError\nfrom composio.utils.safe_path import secure_basename_join\ntry:\n    p = secure_basename_join(base, filename, root=trusted_root)\nexcept UnsafePathComponentError as e:\n    log_security_event(e)\n    p = secure_basename_join(trusted_root, filename, root=trusted_root)","preventionTips":["Pass a constant trusted root as both base and root","Expand ~ and resolve symlinks in base before the call, or better, don't derive base from untrusted input"],"tags":["path-traversal","security","python"],"backgroundTag":"path-traversal-detected","analyzedSha":"64b1b85502b1beeb2379e6c9e8bf1104504fa637","analyzedAt":"2026-08-28T15:39:33.623Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}