{"record":{"id":"c37b264baa8955c4","repo":"affaan-m/ECC","slug":"artifact-path-escapes-output-directory-relative","errorCode":null,"errorMessage":"artifact path escapes output directory: {relative}","messagePattern":"artifact path escapes output directory: (.+?)","errorType":"validation","errorClass":"ContractError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/contract.py","lineNumber":393,"sourceCode":"            raise ContractError(\"artifact path must be a non-empty relative path\")\n        bound_paths.append(relative)\n    if len(bound_paths) != len(set(bound_paths)):\n        raise ContractError(\"receipt contains duplicate artifact paths\")\n    missing = emitted - set(bound_paths)\n    extra = set(bound_paths) - emitted\n    if missing:\n        raise ContractError(f\"unbound emitted artifact: {sorted(missing)}\")\n    if extra:\n        raise ContractError(f\"receipt binds missing artifact: {sorted(extra)}\")\n\n    for entry in entries:\n        relative = entry.get(\"path\")\n        assert isinstance(relative, str)\n        path = (out_dir / relative).resolve()\n        try:\n            path.relative_to(out_dir)\n        except ValueError as error:\n            raise ContractError(f\"artifact path escapes output directory: {relative}\") from error\n        if entry.get(\"provider_execution\") is not False:\n            raise ContractError(f\"artifact {relative} permits provider execution\")\n        if not isinstance(entry.get(\"genre_numbers\"), list):\n            raise ContractError(f\"artifact {relative} lacks genre binding\")\n        modalities = entry.get(\"modalities\")\n        if (not isinstance(modalities, list)\n                or any(modality not in _REQUIRED_MODALITIES for modality in modalities)):\n            raise ContractError(f\"artifact {relative} has invalid modality binding\")\n        if entry.get(\"bytes\") != path.stat().st_size:\n            raise ContractError(f\"artifact {relative} byte size does not match receipt\")\n        if entry.get(\"sha256\") != _sha256(path):\n            raise ContractError(f\"artifact {relative} SHA-256 does not match receipt\")\n        provenance = entry.get(\"provenance\")\n        if not isinstance(provenance, list) or not provenance:\n            raise ContractError(f\"artifact {relative} lacks exact reference/time provenance\")\n        for source in provenance:\n            if not isinstance(source.get(\"reference_path\"), str) or not source[\"reference_path\"]:\n                raise ContractError(f\"artifact {relative} has invalid reference path\")","sourceCodeStart":375,"sourceCodeEnd":411,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/contract.py#L375-L411","documentation":"Each receipt artifact path is resolved against the output directory and must remain inside it (path.relative_to(out_dir) must succeed). This ContractError is raised when a receipt path resolves outside out_dir — e.g. via '../' segments or absolute-path injection — indicating a path-traversal attempt or a corrupt receipt.","triggerScenarios":"Calling validate_artifact_receipt()/validate_bundle() with a receipt entry whose path contains '../' segments, is an absolute path, or is a symlink-resolving target that lands outside the output directory.","commonSituations":"A maliciously crafted or tampered receipt trying to escape the sandbox; a generation bug writing absolute paths into the receipt; copying entries between bundles with different roots.","solutions":["Fix the receipt entry to use a relative path contained within the output directory.","Regenerate the receipt with tasteforge.","Audit the receipt for suspicious paths (look for '..' or leading '/') before validating; treat any occurrence as tampering."],"exampleFix":"// before\n{\"path\": \"../../etc/passwd\", ...}\n// after\n{\"path\": \"images/hero.png\", ...}","handlingStrategy":"validation","validationCode":"resolved = (out_dir / entry['path']).resolve()\nif not resolved.is_relative_to(out_dir):\n    raise SystemExit(f'path escapes output dir: {entry[\"path\"]}')","typeGuard":"def path_is_contained(out_dir: Path, relative: str) -> bool:\n    try:\n        return (out_dir / relative).resolve().is_relative_to(out_dir)\n    except (ValueError, OSError):\n        return False","tryCatchPattern":"try:\n    validate_artifact_receipt(out_dir, entries)\nexcept ContractError as e:\n    if 'escapes output directory' in str(e):\n        reject_receipt(receipt_path)  # treat as tampering, do not attempt repair\n    else:\n        raise","preventionTips":["Only accept receipts from trusted generation runs; verify signatures/hashes of receipt.json where possible.","Reject any receipt path containing '..' or starting with '/' during intake.","Review receipts from external sources before validating."],"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"}