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

  1. Remove or avoid symlinks inside the root directory, or make root the realpath of the intended container (Path(root).resolve())
  2. Pass root as an absolute, trusted constant
  3. 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

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


AI-assisted analysis of ComposioHQ/composio@64b1b85502 (2026-08-28). Data as JSON: /api/errors/a80d771722143001. Report an issue: GitHub.