{"record":{"id":"503b39bc1ad73919","repo":"affaan-m/ECC","slug":"artifact-path-must-stay-beneath-output-root","errorCode":null,"errorMessage":"artifact path must stay beneath output root","messagePattern":"artifact path must stay beneath output root","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/workflow.py","lineNumber":239,"sourceCode":"            return current\n        except Exception:\n            os.close(current)\n            raise\n\n    def prepare(self, directories: tuple[str, ...]) -> None:\n        \"\"\"Validate every known intermediate before the first artifact write.\"\"\"\n        opened: list[int] = []\n        try:\n            for directory in directories:\n                opened.append(self._open_dir((directory,), create=True))\n        finally:\n            for descriptor in opened:\n                os.close(descriptor)\n\n    def write_json(self, relative: str, payload: Any) -> None:\n        path = Path(relative)\n        if path.is_absolute() or not path.name or any(part in {\".\", \"..\"} for part in path.parts):\n            raise ValueError(\"artifact path must stay beneath output root\")\n        parent_fd = self._open_dir(tuple(path.parts[:-1]), create=False)\n        temporary = f\".{path.name}.tmp-{secrets.token_hex(8)}\"\n        descriptor = -1\n        try:\n            try:\n                existing = os.stat(path.name, dir_fd=parent_fd, follow_symlinks=False)\n            except FileNotFoundError:\n                existing = None\n            if existing is not None and not stat.S_ISREG(existing.st_mode):\n                raise ValueError(f\"output artifact must be a regular file: {relative}\")\n            data = json.dumps(payload, indent=2, sort_keys=True).encode(\"utf-8\") + b\"\\n\"\n            descriptor = os.open(\n                temporary,\n                os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,\n                0o600,\n                dir_fd=parent_fd,\n            )\n            view = memoryview(data)","sourceCodeStart":221,"sourceCodeEnd":257,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/workflow.py#L221-L257","documentation":"write_json validates that the requested relative artifact path is relative, names a real file component, and contains no '.' or '..' segments. This is a fail-closed path-traversal guard so artifacts can never escape the output root.","triggerScenarios":"Passing an absolute path (e.g. '/etc/x.json'), a path with a '..' segment, a trailing-segment-only path like 'dir/' (empty name), or a path with '.' components to write_json(relative, payload).","commonSituations":"Building artifact paths by string concatenation from user/config input; joining an absolute base path with a relative name using os.path.join (which yields the absolute path when the second arg is absolute); template configs that include '../' segments.","solutions":["Ensure the path passed to write_json is relative to the output root and contains no '..' or '.' segments","Use pathlib Path arithmetic on a relative base and assert not path.is_absolute() before calling","Strip/normalize user-supplied filename fragments before constructing artifact paths"],"exampleFix":"# before\nwriter.write_json(os.path.join(base_dir, \"manifest.json\"), payload)  # absolute path\n# after\nrel = Path(\"manifest.json\")\nassert not rel.is_absolute() and \"..\" not in rel.parts\nwriter.write_json(str(rel), payload)","handlingStrategy":"validation","validationCode":"from pathlib import Path\ndef safe_relative(rel: str) -> Path:\n    p = Path(rel)\n    if p.is_absolute() or not p.name or any(part in {\".\", \"..\"} for part in p.parts):\n        raise ValueError(f\"unsafe artifact path: {rel}\")\n    return p","typeGuard":"def is_safe_artifact_path(rel: str) -> bool:\n    p = Path(rel)\n    return (not p.is_absolute() and bool(p.name)\n            and not any(part in {\".\", \"..\"} for part in p.parts))","tryCatchPattern":"try:\n    writer.write_json(relative, payload)\nexcept ValueError as e:\n    logging.error(\"rejected artifact path %r: %s\", relative, e)\n    raise","preventionTips":["Always build artifact paths relative to the output root","Never pass user input directly into artifact path strings","Normalize with Path and check parts before calling"],"tags":["path-traversal","validation","filesystem"],"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"}