ComposioHQ/composio · critical · UnsafePathComponentError
Refusing to build a path from a reserved device name as {lab
Error message
Refusing to build a path from a reserved device name as {label}: {value!r} What it means
assert_safe_path_component rejects Windows reserved DOS device names (CON, PRN, AUX, NUL, COM1-9, LPT1-9, superscript variants) case-insensitively, because writing to one on Windows targets the device instead of a file — on every platform, not just Windows.
Source
Thrown at python/composio/utils/safe_path.py:137
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.
if not SAFE_COMPONENT_REGEX.fullmatch(value):
raise UnsafePathComponentError(
f"Refusing to build a path from an unsafe {label}: {value!r}. "
f"Expected only letters, digits, underscores, and hyphens "
f"(pattern {SAFE_COMPONENT_REGEX.pattern})."
)
if value.upper() in WINDOWS_RESERVED_NAMES:
raise UnsafePathComponentError(
f"Refusing to build a path from a reserved device name as {label}: {value!r}"
)
return value
def safe_basename(name: str, *, label: str = "filename") -> str:
"""Collapse an untrusted filename to a bare, writable basename.
Filenames need their own validator: :func:`assert_safe_path_component`
forbids ``.``, which nearly every real filename contains. This applies the
remaining checks — no separators, no traversal, no NUL, bounded length, no
reserved device name — to the one component a server most directly controls.
``PureWindowsPath`` treats both ``/`` and ``\\`` as separators, so a name
crafted for a Windows target (``..\\..\\evil``) is stripped even when the
SDK runs on POSIX, where ``Path(...).name`` would return it intact.
View on GitHub (pinned to 64b1b85502)
Solutions
- Rename the component (prefix or suffix it, e.g. 'NUL_file' → 'upload_NUL')
- Reject/ignore the untrusted value rather than trying to write it
Example fix
# before secure_join(root, "nul") # after secure_join(root, "download_nul")
Defensive patterns
Strategy: validation
Validate before calling
from composio.utils.safe_path import WINDOWS_RESERVED_NAMES
def not_reserved(v):
return v.upper() not in WINDOWS_RESERVED_NAMES Type guard
from composio.utils.safe_path import WINDOWS_RESERVED_NAMES
def is_non_device_name(v: str) -> bool:
return v.upper() not in WINDOWS_RESERVED_NAMES Try / catch
from composio.exceptions import UnsafePathComponentError
try:
p = secure_join(root, name)
except UnsafePathComponentError:
p = secure_join(root, 'dl_' + name) Prevention
- Prefix device-like names ('nul', 'com1') before using them as components
- Remember the check is case-insensitive and cross-platform
When it happens
Trigger: secure_join(root, 'NUL'), secure_join(root, 'com1'), secure_join(root, 'AUX') — an untrusted component matching a reserved device name in any case.
Common situations: Malicious or unlucky IDs/downloads named after devices; tests exercising the guardrail; cross-platform code where a POSIX-only check would let these through.
Related errors
- Refusing to write {label} that is a reserved device name: {n
- 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
- Refusing to build a path from an unsafe {label}: {value!r}.
AI-assisted analysis of ComposioHQ/composio@64b1b85502 (2026-08-28).
Data as JSON: /api/errors/ba391b62833ac5ce.
Report an issue: GitHub.