ComposioHQ/composio · critical · UnsafePathComponentError

Refusing to build a path from a {label} containing path sepa

Error message

Refusing to build a path from a {label} containing path separators or a drive letter: {value!r}

What it means

assert_safe_path_component rejects a component containing path separators (/ or \) or a Windows drive letter (C:). PureWindowsPath is used so both separator styles are caught on every platform, preventing ../x and ..\x traversal.

Source

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

    component, else raise.

    Fails closed. Rejects traversal (``..``), separators of either platform,
    absolute paths, drive letters, NUL bytes, reserved device names, and
    anything outside :data:`SAFE_COMPONENT_REGEX`.

    :raises UnsafePathComponentError: when ``value`` is unsafe.
    """
    if not isinstance(value, str) or not value:
        raise UnsafePathComponentError(
            f"Refusing to build a path from an empty or non-string {label}: {value!r}"
        )

    # `PureWindowsPath` treats both `/` and `\` as separators, so a single check
    # catches `../x` and `..\x` regardless of the host platform. A slug crafted
    # for a Windows target must not slip through on a POSIX build machine.
    as_windows_path = PureWindowsPath(value)
    if len(as_windows_path.parts) != 1 or as_windows_path.anchor:
        raise UnsafePathComponentError(
            f"Refusing to build a path from a {label} containing path separators "
            f"or a drive letter: {value!r}"
        )

    if len(value) > MAX_COMPONENT_LENGTH:
        raise UnsafePathComponentError(
            f"Refusing to build a path from a {label} longer than "
            f"{MAX_COMPONENT_LENGTH} characters: {value[:32]!r}... "
            f"({len(value)} characters)"
        )

    # `.` and `..` are excluded by the regex (no `.` in the character class),
    # as are NUL bytes and every separator. The explicit checks above exist to
    # produce a precise error message rather than a generic pattern mismatch.
    #
    # `fullmatch`, not `match`: in a `match`, `$` also matches just before a
    # single trailing newline, so `"GMAIL\n"` would satisfy `^[A-Za-z0-9_-]+$`
    # and reach the filesystem with a control character in the name.

View on GitHub (pinned to 64b1b85502)

Solutions

  1. Pass only the final directory name; build multi-level paths as separate safe components to secure_join
  2. If a real subpath is required, split it yourself and validate each segment
  3. Sanitize untrusted input before it reaches path building

Example fix

# before
secure_join(root, "a/b/c")
# after
secure_join(root, "a", "b", "c")
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import PureWindowsPath
def is_single_component(v):
    p = PureWindowsPath(v)
    return isinstance(v, str) and len(p.parts) == 1 and not p.anchor

Type guard

def is_traversal_free(v) -> bool:
    return isinstance(v, str) and '/' not in v and '\\' not in v and ':' not in v and v not in ('.','..')

Try / catch

from composio.exceptions import UnsafePathComponentError
try:
    p = secure_join(root, name)
except UnsafePathComponentError:
    name = name.replace('/','_').replace('\\','_')
    p = secure_join(root, name)

Prevention

When it happens

Trigger: secure_join(root, '../etc/passwd'), secure_join(root, 'C:\\boot.ini'), or any component with an anchor/multiple parts — i.e. traversal payloads or components that were meant to be joined with the root instead.

Common situations: Passing relative file paths where a single directory component is expected; untrusted API fields containing traversal strings; accidentally passing a full path as a slug.

Related errors


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