{"record":{"id":"679781021384571a","repo":"affaan-m/ECC","slug":"output-path-contains-an-invalid-component","errorCode":null,"errorMessage":"output path contains an invalid component","messagePattern":"output path contains an invalid component","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/workflow.py","lineNumber":202,"sourceCode":"            root.mkdir(mode=0o700)\n        self.root = root\n        self._root_fd = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)\n        self._written: list[str] = []\n\n    def close(self) -> None:\n        if self._root_fd >= 0:\n            os.close(self._root_fd)\n            self._root_fd = -1\n\n    def __del__(self) -> None:\n        self.close()\n\n    def _open_dir(self, parts: tuple[str, ...], *, create: bool) -> int:\n        current = os.dup(self._root_fd)\n        try:\n            for part in parts:\n                if not part or part in {\".\", \"..\"} or \"/\" in part:\n                    raise ValueError(\"output path contains an invalid component\")\n                try:\n                    metadata = os.stat(part, dir_fd=current, follow_symlinks=False)\n                except FileNotFoundError:\n                    if not create:\n                        raise ValueError(f\"missing output directory: {part}\") from None\n                    os.mkdir(part, mode=0o700, dir_fd=current)\n                    metadata = os.stat(part, dir_fd=current, follow_symlinks=False)\n                if stat.S_ISLNK(metadata.st_mode):\n                    raise ValueError(f\"output directory must not be a symlink: {part}\")\n                if not stat.S_ISDIR(metadata.st_mode):\n                    raise ValueError(f\"output intermediate must be a directory: {part}\")\n                child = os.open(\n                    part,\n                    os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,\n                    dir_fd=current,\n                )\n                os.close(current)\n                current = child","sourceCodeStart":184,"sourceCodeEnd":220,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/workflow.py#L184-L220","documentation":"_SafeOutput._open_dir walks the output tree component by component using dir_fd-relative operations, and validates each component name. A component that is empty, is '.' or '..', or contains '/' (path-separator injection) is rejected to guarantee the final path cannot escape the anchored root directory. This is a path-traversal guard for every prepared/written artifact.","triggerScenarios":"Calling prepare/write_json/artifact_metadata with a relative path containing '../', a leading '/', an embedded '/', or an empty segment (e.g. 'out//file.json', './x', 'sub/../file.json').","commonSituations":"Building paths from user input or untrusted metadata (filenames from probe data); joining an absolute path with the output root and passing the concatenation; empty filename variables from failed templates.","solutions":["Sanitize components: reject/split any name containing '/', and drop '', '.', '..' segments before calling the API","Use pathlib to derive pure relative names: name = Path(untrusted).name, then pass parts individually","Never pass absolute paths into write_json/prepare — the root is the anchor; pass paths relative to it","Validate user-supplied artifact names against a strict pattern (e.g. ^[A-Za-z0-9._-]+$)"],"exampleFix":"# before\nsafe.write_json(user_supplied_relpath, data)  # e.g. '../etc/evil.json'\n\n# after\nparts = tuple(p for p in PurePosixPath(user_supplied_relpath).parts\n              if p not in ('', '.', '..') and '/' not in p)\nif not parts or any(p.startswith('/') for p in parts):\n    raise ValueError('unsafe artifact path')\nsafe.write_json(parts, data)","handlingStrategy":"validation","validationCode":"import re\nSAFE_COMPONENT = re.compile(r'^[A-Za-z0-9][A-Za-z0-9._-]*$')\ndef safe_parts(relpath):\n    parts = [p for p in PurePosixPath(relpath).parts]\n    if not parts or any(not SAFE_COMPONENT.match(p) for p in parts):\n        raise ValueError(f'unsafe artifact path: {relpath!r}')\n    return tuple(parts)","typeGuard":"def is_safe_component(p: str) -> bool:\n    return bool(p) and p not in ('.', '..') and '/' not in p and not p.startswith('/')","tryCatchPattern":"try:\n    safe.write_json(parts, data)\nexcept ValueError as e:\n    if 'invalid component' in str(e):\n        print(f'Rejecting untrusted path: {parts!r} ({e})')\n    else:\n        raise","preventionTips":["Never pass absolute or user-controlled raw paths into write_json/prepare","Normalize with PurePosixPath(...).name to strip traversal segments","Allowlist artifact names with a strict regex","Treat filenames from probe/metadata as untrusted input"],"tags":["security","path-traversal","validation"],"backgroundTag":"path-traversal-blocked","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}