ComposioHQ/composio · critical · UnsafePathComponentError

Path traversal detected: {label} {name!r} leaves no usable b

Error message

Path traversal detected: {label} {name!r} leaves no usable basename to write to.

What it means

safe_basename found no usable basename after taking PureWindowsPath(name).name — the value was empty, whitespace-only, all dots, or pure separators. Such a name would resolve to the base directory itself and surface as IsADirectoryError at write time, so it is treated as path traversal.

Source

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

    SDK runs on POSIX, where ``Path(...).name`` would return it intact.

    Names that leave no usable basename are refused rather than replaced with a
    generated one: a response that cannot name its own file is malformed or
    hostile, and inventing a name would hide that. ``.`` and the empty string
    both basename to ``""``, which makes an output path equal to its own
    directory and surfaces as ``IsADirectoryError`` at write time.

    :raises UnsafePathComponentError: when ``name`` yields no usable basename or
        is unsafe to write.
    """
    if not isinstance(name, str):
        raise UnsafePathComponentError(
            f"Refusing to write a non-string {label}: {name!r}"
        )

    raw_basename = PureWindowsPath(name).name
    if not raw_basename or not raw_basename.strip() or set(raw_basename) == {"."}:
        raise UnsafePathComponentError(
            f"Path traversal detected: {label} {name!r} leaves no usable "
            "basename to write to."
        )
    if "\x00" in raw_basename:
        raise UnsafePathComponentError(
            f"Refusing to write {label} containing a NUL byte: {name!r}"
        )
    if any(ord(char) < 32 or char in '<>:"|?*' for char in raw_basename):
        raise UnsafePathComponentError(
            f"Refusing to write {label} containing characters reserved by "
            f"Windows: {name!r}"
        )
    if raw_basename.endswith((" ", ".")):
        raise UnsafePathComponentError(
            f"Refusing to write {label} ending in a space or dot: {name!r}"
        )

    basename = raw_basename.strip()

View on GitHub (pinned to 64b1b85502)

Solutions

  1. Fall back to a server-generated filename when extraction yields nothing
  2. Reject the download/request carrying the empty name

Example fix

# before
secure_basename_join(base, fname)  # fname == '..'
# after
if not fname or set(fname) <= {'.', ' ', '/', '\\'}:
    fname = f"download-{uuid.uuid4().hex}"
secure_basename_join(base, fname)
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import PureWindowsPath
def has_usable_basename(v):
    b = PureWindowsPath(v).name if isinstance(v, str) else ''
    return bool(b.strip()) and set(b) != {'.'}

Type guard

def is_writable_name(v: str) -> bool:
    b = PureWindowsPath(v).name
    return bool(b) and bool(b.strip()) and set(b) != {'.'}

Try / catch

from composio.exceptions import UnsafePathComponentError
from uuid import uuid4
try:
    p = secure_basename_join(base, name)
except UnsafePathComponentError:
    p = secure_basename_join(base, f'file-{uuid4().hex}')

Prevention

When it happens

Trigger: secure_basename_join(base, ''), (base, ' '), (base, '...'), (base, '/'), (base, '..') — the basename extraction yields nothing writable.

Common situations: Empty filename fields from APIs; values that are entirely separator/dot characters crafted to escape to a parent or root.

Related errors


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