ComposioHQ/composio · critical · UnsafePathComponentError

Path traversal detected: {label} {name!r} resolves to {candi

Error message

Path traversal detected: {label} {name!r} resolves to {candidate.resolve()}, which is outside {resolved_root}.

What it means

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.

Source

Thrown at python/composio/utils/safe_path.py:253

) -> Path:
    """Join a single untrusted filename under ``base``, contained within ``root``.

    The filename counterpart to :func:`secure_join`, which cannot be reused here
    because it forbids ``.`` — correct for a slug, wrong for ``report.pdf``.

    ``root`` defaults to ``base`` but is separate for the download path, where
    ``base`` is a per-tool subdirectory that untrusted slugs helped build and
    only the configured ``root`` above it is trusted. Anchoring on ``base``
    there would check the result against a directory those slugs had moved.

    :raises UnsafePathComponentError: when ``name`` is unsafe, or when the
        result escapes ``root``.
    """
    resolved_base = resolve_root(base)
    resolved_root = resolved_base if root is None else resolve_root(root)
    candidate = resolved_base / safe_basename(name, label=label)
    if not is_inside_dir(candidate.resolve(), resolved_root):
        raise UnsafePathComponentError(
            f"Path traversal detected: {label} {name!r} resolves to "
            f"{candidate.resolve()}, which is outside {resolved_root}."
        )
    return candidate


def secure_join(root: t.Union[str, Path], *components: str) -> Path:
    """Join untrusted ``components`` beneath the trusted ``root``.

    ``root`` is the sole anchor of trust and must not itself be derived from
    untrusted input — that is the whole point. Each component is validated by
    :func:`assert_safe_path_component`, then the joined result is resolved and
    re-checked against the resolved root. The second check is belt-and-braces:
    it catches a symlink inside ``root`` pointing outside it, which per-component
    validation cannot see.

    Performs no filesystem writes; the caller creates directories only after
    this returns.

View on GitHub (pinned to 64b1b85502)

Solutions

  1. Pass a constant, trusted root directory as both base and root (secure_basename_join(root, name, root=root))
  2. Never derive base from untrusted input; anchor containment on a literal/module constant
  3. Ensure root is the resolved form of the directory you intend to contain writes to

Example fix

# before
secure_basename_join(api_base_dir, filename, root=my_root)
# after
secure_basename_join(my_root, filename, root=my_root)
Defensive patterns

Strategy: try-catch

Validate before calling

from composio.utils.safe_path import resolve_root, is_inside_dir
from pathlib import Path
def join_stays_inside(base, name, root):
    cand = (resolve_root(base) / Path(name).name)
    return is_inside_dir(cand.resolve(), resolve_root(root))

Try / catch

from composio.exceptions import UnsafePathComponentError
from composio.utils.safe_path import secure_basename_join
try:
    p = secure_basename_join(base, filename, root=trusted_root)
except UnsafePathComponentError as e:
    log_security_event(e)
    p = secure_basename_join(trusted_root, filename, root=trusted_root)

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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