{"record":{"id":"b5d9137d4f65e93f","repo":"affaan-m/ECC","slug":"artifact-path-must-be-canonical-and-absolute","errorCode":null,"errorMessage":"artifact path must be canonical and absolute","messagePattern":"artifact path must be canonical and absolute","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/integration.py","lineNumber":116,"sourceCode":"def _parent_fd(path: Path) -> int:\n    flags = os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_DIRECTORY\n    parent = os.open(path.anchor, flags)\n    try:\n        for part in path.parts[1:-1]:\n            child = os.open(part, flags, dir_fd=parent)\n            os.close(parent)\n            parent = child\n        return parent\n    except BaseException:\n        os.close(parent)\n        raise\n\n\ndef _read_local(raw: str, *, parse_json: bool, expected_size: int | None = None,\n                expected_hash: str | None = None) -> Any:\n    path = Path(raw)\n    if not path.is_absolute() or str(path) != raw or \"..\" in path.parts:\n        raise ValueError(\"artifact path must be canonical and absolute\")\n    parent = descriptor = None\n    try:\n        flags = os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK\n        parent = _parent_fd(path)\n        before = os.stat(path.name, dir_fd=parent, follow_symlinks=False)\n        if not stat.S_ISREG(before.st_mode) or getattr(before, \"st_flags\", 0) & 0x40000000:\n            raise ValueError(\"artifact must be a resident regular file\")\n        if expected_size is None:\n            expected_size = before.st_size\n        if parse_json and expected_size > _MAX_JSON:\n            raise ValueError(\"JSON artifact exceeds local size limit\")\n        if before.st_size != expected_size:\n            raise ValueError(\"artifact byte count mismatch\")\n        descriptor = os.open(path.name, flags, dir_fd=parent)\n        if _identity(before) != _identity(os.fstat(descriptor)):\n            raise ValueError(\"artifact changed before reading\")\n        digest, chunks, count = hashlib.sha256(), [], 0\n        while data := os.read(descriptor, 65536):","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/integration.py#L98-L134","documentation":"`_read_local` refuses to open any artifact whose path string is not already a canonical absolute path. It rejects relative paths, symlink-style segments such as `..`, and non-canonical spellings (e.g. trailing slashes, `//`, `/a/./b`). The library throws this to guarantee that the TOCTOU-safe `dir_fd`-based reading logic operates on exactly one unambiguous filesystem location.","triggerScenarios":"Passing a relative path like `build/artifact.json`; a path containing `..` (e.g. `/tmp/build/../artifact.json`); a non-normalized string such as `/tmp//artifact.json` or `/tmp/artifact.json/` to `_read_local` via `_artifact` or `load_application_request`.","commonSituations":"Building paths by string concatenation instead of `os.path.abspath`/`Path.resolve()`; accepting a user-supplied path from a CLI flag or config and forwarding it as-is; environment-relative defaults (`~`, `$VAR`) left unexpanded.","solutions":["Normalize and absolutize first: `raw = str(Path(raw).expanduser().resolve())`, ensuring it does not escape the intended directory.","Use `os.path.realpath` to canonicalize, then confirm `\"..\" not in Path(raw).parts`.","Reject or resolve relative paths at the configuration boundary (CLI parser / config loader) before they reach the artifact reader."],"exampleFix":"// before\nload_application_request(\"../out/request.json\")\n// after\nfrom pathlib import Path\ncanonical = str(Path(\"../out/request.json\").expanduser().resolve())\nload_application_request(canonical)","handlingStrategy":"validation","validationCode":"from pathlib import Path, PurePosixPath\ndef canonical_absolute(raw: str) -> str:\n    p = Path(raw).expanduser().resolve()\n    s = str(p)\n    if not p.is_absolute() or s != raw and \"..\" in PurePosixPath(raw).parts:\n        raise ValueError(f\"path must be canonical and absolute: {raw!r}\")\n    return s","typeGuard":"def is_canonical_absolute(raw: str) -> bool:\n    from pathlib import Path\n    p = Path(raw)\n    return p.is_absolute() and str(p) == raw and \"..\" not in p.parts","tryCatchPattern":"try:\n    req = load_application_request(raw_path)\nexcept ValueError as e:\n    if str(e) == \"artifact path must be canonical and absolute\":\n        req = load_application_request(str(Path(raw_path).expanduser().resolve()))\n    else:\n        raise","preventionTips":["Never build artifact paths by string concatenation; use Path.resolve()/os.path.realpath.","Normalize paths once, at the boundary where users or configs supply them.","Reject user-supplied paths containing `..` at input-validation time, not at read time.","Expand `~` and environment variables explicitly before passing paths into the library."],"tags":["filesystem","path-validation","security"],"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"}