ComposioHQ/composio · critical · UnsafePathComponentError

Refusing to write {label} that is a reserved device name: {n

Error message

Refusing to write {label} that is a reserved device name: {name!r}

What it means

safe_basename rejects basenames whose stem before the first dot (trailing spaces stripped, uppercased) is a Windows reserved device name — e.g. 'NUL.tar.gz' or 'com1.txt' — because Windows opens the device, not a file, no matter how many extensions follow.

Source

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

    basename = raw_basename.strip()
    try:
        encoded_length = len(os.fsencode(basename))
    except UnicodeEncodeError as e:
        raise UnsafePathComponentError(
            f"Refusing to write {label} containing invalid Unicode: {name!r}"
        ) from e
    if encoded_length > MAX_COMPONENT_LENGTH:
        raise UnsafePathComponentError(
            f"Refusing to write {label} longer than {MAX_COMPONENT_LENGTH} bytes: "
            f"{basename[:32]!r}... ({encoded_length} bytes)"
        )
    # Compare everything before the first dot: on Windows `NUL.tar.gz` opens
    # the null device just as `NUL` does, so any number of extensions provides
    # no protection.
    device_name = basename.split(".", 1)[0].rstrip(" ").upper()
    if device_name in WINDOWS_RESERVED_NAMES:
        raise UnsafePathComponentError(
            f"Refusing to write {label} that is a reserved device name: {name!r}"
        )
    return basename


def resolve_root(root: t.Union[str, Path]) -> Path:
    """Normalize a trusted root to an absolute, symlink-resolved path.

    Every containment check must derive its anchor through this one function.
    When two call sites normalize differently — one expanding ``~`` and one not
    — the check compares mismatched paths and rejects legitimate writes while
    reporting them as attacks. Sharing the normalization is what keeps the two
    ends of a containment check comparable.
    """
    expanded = Path(root).expanduser()
    try:
        return expanded.resolve(strict=False)
    except OSError:

View on GitHub (pinned to 64b1b85502)

Solutions

  1. Prefix the filename (e.g. 'download-NUL.tar.gz') so the stem no longer matches
  2. Reject the untrusted download carrying the device name

Example fix

# before
secure_basename_join(base, 'nul.tar.gz')
# after
secure_basename_join(base, 'download-nul.tar.gz')
Defensive patterns

Strategy: validation

Validate before calling

from composio.utils.safe_path import WINDOWS_RESERVED_NAMES
def stem_not_device(v):
    return PureWindowsPath(v).name.split('.', 1)[0].rstrip(' ').upper() not in WINDOWS_RESERVED_NAMES

Type guard

from composio.utils.safe_path import WINDOWS_RESERVED_NAMES
def is_not_device_filename(v: str) -> bool:
    return v.split('.', 1)[0].rstrip(' ').upper() not in WINDOWS_RESERVED_NAMES

Try / catch

from composio.exceptions import UnsafePathComponentError
try:
    p = secure_basename_join(base, name)
except UnsafePathComponentError:
    p = secure_basename_join(base, 'file-' + name)

Prevention

When it happens

Trigger: secure_basename_join(base, 'NUL.tar.gz'), (base, 'COM1.json'), (base, 'aux .bak') — any filename whose first dot-delimited segment matches a reserved device name.

Common situations: Downloads named after devices (logs named 'prn.log', ports named 'com1.csv'); crafted filenames targeting Windows device files.

Related errors


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