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

  1. Rename the component (prefix or suffix it, e.g. 'NUL_file' → 'upload_NUL')
  2. 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

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


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