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
- Pass only the final directory name; build multi-level paths as separate safe components to secure_join
- If a real subpath is required, split it yourself and validate each segment
- 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
- Pass each directory level as its own component to secure_join
- Treat any separator in a slug as malicious input, not a path
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
- Unsafe path component: {e}
- Refusing to build a path from an empty or non-string {label}
- Path traversal detected: {label} {name!r} leaves no usable b
- Path traversal detected: {label} {name!r} resolves to {candi
- Refusing to write outside the configured directory: {compone
AI-assisted analysis of ComposioHQ/composio@64b1b85502 (2026-08-28).
Data as JSON: /api/errors/a7e35c910047e4e9.
Report an issue: GitHub.