{"record":{"id":"a80d771722143001","repo":"ComposioHQ/composio","slug":"refusing-to-write-outside-the-configured-directory","errorCode":null,"errorMessage":"Refusing to write outside the configured directory: {components!r} resolves to {resolved_candidate}, which is outside {resolved_root}.","messagePattern":"Refusing to write outside the configured directory: (.+?) resolves to (.+?), which is outside (.+?)\\.","errorType":"exception","errorClass":"UnsafePathComponentError","httpStatus":null,"severity":"critical","filePath":"python/composio/utils/safe_path.py","lineNumber":289,"sourceCode":"    this returns.\n\n    :raises UnsafePathComponentError: when any component is unsafe, or when the\n        joined path escapes ``root``.\n    \"\"\"\n    resolved_root = resolve_root(root)\n\n    safe_components = [\n        assert_safe_path_component(component) for component in components\n    ]\n\n    candidate = resolved_root.joinpath(*safe_components)\n    try:\n        resolved_candidate = candidate.resolve(strict=False)\n    except OSError:\n        resolved_candidate = candidate\n\n    if not is_inside_dir(resolved_candidate, resolved_root):\n        raise UnsafePathComponentError(\n            f\"Refusing to write outside the configured directory: \"\n            f\"{components!r} resolves to {resolved_candidate}, \"\n            f\"which is outside {resolved_root}.\"\n        )\n\n    return resolved_candidate\n","sourceCodeStart":271,"sourceCodeEnd":296,"githubUrl":"https://github.com/ComposioHQ/composio/blob/64b1b85502b1beeb2379e6c9e8bf1104504fa637/python/composio/utils/safe_path.py#L271-L296","documentation":"secure_join's final containment check failed: after validating each component and resolving the candidate path, it resolves outside the trusted root, so the write is refused before any mkdir/open happens. This guards against escapes the per-component checks can't see (symlinks inside the root, weird resolve behavior).","triggerScenarios":"secure_join(root, *components) where an existing symlink inside root points outside, or a component set that resolves (strict=False) beyond resolved_root; also directly tested via _download_file_value and the secure_join tests.","commonSituations":"Workspace/cache directories containing symlinks to outside locations (e.g. symlinked node_modules or a linked downloads dir); root derived from untrusted input; root passed as a relative path that resolves differently than expected.","solutions":["Remove or avoid symlinks inside the root directory, or make root the realpath of the intended container (Path(root).resolve())","Pass root as an absolute, trusted constant","If symlinks are intentional, set root to the outermost directory that legitimately contains all resolved targets"],"exampleFix":"# before\nsecure_join(Path('workspace'), *components)  # relative root\n# after\nsecure_join(Path('workspace').resolve(), *components)","handlingStrategy":"validation","validationCode":"from pathlib import Path\ndef root_is_safe(root):\n    r = Path(root).resolve()\n    return r.is_dir() and not any(p.is_symlink() for p in r.parents)\ndef no_dangling_symlinks(root):\n    return not any(p.is_symlink() and not p.resolve().is_relative_to(Path(root).resolve()) for p in Path(root).rglob('*') if p.is_symlink())","typeGuard":null,"tryCatchPattern":"from composio.exceptions import UnsafePathComponentError\ntry:\n    p = secure_join(root, *components)\nexcept UnsafePathComponentError as e:\n    log_security_event(e)\n    raise","preventionTips":["Always pass an absolute, resolved, constant root to secure_join","Keep symlinks out of SDK-managed directories, or set root to the outer real container","Never let untrusted input influence the root argument"],"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"}