ComposioHQ/composio · critical · UnsafePathComponentError
Refusing to write outside the configured directory: {compone
Error message
Refusing to write outside the configured directory: {components!r} resolves to {resolved_candidate}, which is outside {resolved_root}. What it means
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).
Source
Thrown at python/composio/utils/safe_path.py:289
this returns.
:raises UnsafePathComponentError: when any component is unsafe, or when the
joined path escapes ``root``.
"""
resolved_root = resolve_root(root)
safe_components = [
assert_safe_path_component(component) for component in components
]
candidate = resolved_root.joinpath(*safe_components)
try:
resolved_candidate = candidate.resolve(strict=False)
except OSError:
resolved_candidate = candidate
if not is_inside_dir(resolved_candidate, resolved_root):
raise UnsafePathComponentError(
f"Refusing to write outside the configured directory: "
f"{components!r} resolves to {resolved_candidate}, "
f"which is outside {resolved_root}."
)
return resolved_candidate
View on GitHub (pinned to 64b1b85502)
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
Example fix
# before
secure_join(Path('workspace'), *components) # relative root
# after
secure_join(Path('workspace').resolve(), *components) Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path
def root_is_safe(root):
r = Path(root).resolve()
return r.is_dir() and not any(p.is_symlink() for p in r.parents)
def no_dangling_symlinks(root):
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()) Try / catch
from composio.exceptions import UnsafePathComponentError
try:
p = secure_join(root, *components)
except UnsafePathComponentError as e:
log_security_event(e)
raise Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Unsafe path component: {e}
- Refusing to build a path from an empty or non-string {label}
- Refusing to build a path from a {label} containing path sepa
- Path traversal detected: {label} {name!r} leaves no usable b
- Path traversal detected: {label} {name!r} resolves to {candi
AI-assisted analysis of ComposioHQ/composio@64b1b85502 (2026-08-28).
Data as JSON: /api/errors/a80d771722143001.
Report an issue: GitHub.