{"record":{"id":"b267492be1583090","repo":"ComposioHQ/composio","slug":"refusing-to-write-label-ending-in-a-space-or-dot","errorCode":null,"errorMessage":"Refusing to write {label} ending in a space or dot: {name!r}","messagePattern":"Refusing to write (.+?) ending in a space or dot: (.+?)","errorType":"exception","errorClass":"UnsafePathComponentError","httpStatus":null,"severity":"warning","filePath":"python/composio/utils/safe_path.py","lineNumber":186,"sourceCode":"        )\n\n    raw_basename = PureWindowsPath(name).name\n    if not raw_basename or not raw_basename.strip() or set(raw_basename) == {\".\"}:\n        raise UnsafePathComponentError(\n            f\"Path traversal detected: {label} {name!r} leaves no usable \"\n            \"basename to write to.\"\n        )\n    if \"\\x00\" in raw_basename:\n        raise UnsafePathComponentError(\n            f\"Refusing to write {label} containing a NUL byte: {name!r}\"\n        )\n    if any(ord(char) < 32 or char in '<>:\"|?*' for char in raw_basename):\n        raise UnsafePathComponentError(\n            f\"Refusing to write {label} containing characters reserved by \"\n            f\"Windows: {name!r}\"\n        )\n    if raw_basename.endswith((\" \", \".\")):\n        raise UnsafePathComponentError(\n            f\"Refusing to write {label} ending in a space or dot: {name!r}\"\n        )\n\n    basename = raw_basename.strip()\n    try:\n        encoded_length = len(os.fsencode(basename))\n    except UnicodeEncodeError as e:\n        raise UnsafePathComponentError(\n            f\"Refusing to write {label} containing invalid Unicode: {name!r}\"\n        ) from e\n    if encoded_length > MAX_COMPONENT_LENGTH:\n        raise UnsafePathComponentError(\n            f\"Refusing to write {label} longer than {MAX_COMPONENT_LENGTH} bytes: \"\n            f\"{basename[:32]!r}... ({encoded_length} bytes)\"\n        )\n    # Compare everything before the first dot: on Windows `NUL.tar.gz` opens\n    # the null device just as `NUL` does, so any number of extensions provides\n    # no protection.","sourceCodeStart":168,"sourceCodeEnd":204,"githubUrl":"https://github.com/ComposioHQ/composio/blob/64b1b85502b1beeb2379e6c9e8bf1104504fa637/python/composio/utils/safe_path.py#L168-L204","documentation":"safe_basename rejects filenames ending in a space or a dot ('file ', 'file.'). Windows silently strips trailing spaces/dots, which breaks round-tripping and can collide with other names, so such basenames are refused up front.","triggerScenarios":"secure_basename_join(base, name) where the extracted basename ends with ' ' or '.' — e.g. 'my report ' or 'data.'.","commonSituations":"Copy-pasted filenames with invisible trailing whitespace; API-supplied names with trailing dots from truncated extensions.","solutions":["Strip trailing spaces and dots from the filename before passing it in (note the SDK itself only strips for internal checks, the write uses raw basename)","Generate a normalized filename"],"exampleFix":"# before\nsecure_basename_join(base, 'data.')\n# after\nsecure_basename_join(base, 'data.'.rstrip(' .'))","handlingStrategy":"validation","validationCode":"def no_trailing_space_dot(v):\n    return isinstance(v, str) and not v.rstrip().rstrip('.').endswith((' ', '.')) and not PureWindowsPath(v).name.endswith((' ', '.'))","typeGuard":"from pathlib import PureWindowsPath\ndef is_trimmed_name(v: str) -> bool:\n    return not PureWindowsPath(v).name.endswith((' ', '.'))","tryCatchPattern":"from composio.exceptions import UnsafePathComponentError\ntry:\n    p = secure_basename_join(base, name)\nexcept UnsafePathComponentError:\n    p = secure_basename_join(base, name.rstrip(' .'))","preventionTips":["Strip trailing whitespace/dots from user-supplied filenames","Watch for copy-pasted names with invisible trailing spaces"],"tags":["windows","filename-validation","python"],"backgroundTag":"invalid-filename-characters","analyzedSha":"64b1b85502b1beeb2379e6c9e8bf1104504fa637","analyzedAt":"2026-08-28T15:39:33.623Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}