{"record":{"id":"aa0dfa187ae457dd","repo":"abhigyanpatwari/GitNexus","slug":"label-escapes-its-allowed-repository-root-rela","errorCode":null,"errorMessage":"{label} escapes its allowed repository root: {relative}","messagePattern":"(.+?) escapes its allowed repository root: (.+?)","errorType":"exception","errorClass":"SandboxError","httpStatus":null,"severity":"error","filePath":"eval/workflow_bench/proposer_sandbox.py","lineNumber":570,"sourceCode":"    try:\n        resolved = lexical.resolve(strict=True)\n    except OSError as exc:\n        raise SandboxError(f\"{label} must be a real directory: {lexical}: {exc}\") from exc\n    if resolved != lexical:\n        raise SandboxError(f\"{label} must not traverse symlinks: {lexical}\")\n    return lexical\n\n\ndef _safe_repo_source(repo: Path, relative: str, *, label: str) -> tuple[Path, Path]:\n    candidate = PurePosixPath(relative)\n    if candidate.is_absolute() or \"..\" in candidate.parts or not candidate.parts:\n        raise SandboxError(f\"{label} must be a repository-relative path: {relative!r}\")\n    lexical = repo / Path(*candidate.parts)\n    resolved = lexical.resolve()\n    try:\n        resolved.relative_to(repo)\n    except ValueError as exc:\n        raise SandboxError(f\"{label} escapes its allowed repository root: {relative}\") from exc\n    if not resolved.exists():\n        raise SandboxError(f\"{label} does not exist: {relative}\")\n    return lexical, resolved\n\n\ndef _prepare_clone_target(\n    clone: Path,\n    relative: PurePosixPath,\n    *,\n    directory: bool | None,\n    label: str,\n) -> Path:\n    \"\"\"Validate/create a clone-local target without following any symlink.\n\n    This runs before Bubblewrap, so ordinary ``Path.mkdir``/``touch`` calls\n    are not acceptable: an untrusted tracked parent symlink could redirect a\n    mount placeholder write into the host filesystem.\n    \"\"\"","sourceCodeStart":552,"sourceCodeEnd":588,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/d540b00184d71a896261ee02670da9a92d59d8f7/eval/workflow_bench/proposer_sandbox.py#L552-L588","documentation":"Raised by _safe_repo_source when the relative path is structurally valid (not absolute, no '..') but lexical.resolve() lands outside the repository root — resolve().relative_to(repo) raised ValueError. This catches escapes that survive the structural check, e.g. a real symlink inside the repo that points outside, or a path component that resolves to a parent via a symlink.","triggerScenarios":"A repo-relative path with no '..' that nonetheless resolves outside repo because an intermediate component is a symlink to an external location, e.g. repo/asset where asset -> /etc. resolve().relative_to(repo) then raises ValueError.","commonSituations":"A symlink inside the workspace points outside (supply-chain or accidental); a repo contains a submodule or linked asset that resolves above the root; a mount/copy of assets introduced a symlink escaping the repo; a test fixture planted a symlink to shared storage; an attacker/proposer planted an escaping symlink as a traversal attempt.","solutions":["Remove or replace the escaping symlink inside the repo with the real file/dir copied in.","Validate repo contents for escaping symlinks before staging: reject any entry whose resolved target is not within repo.","Re-clone the repo from a trusted source to drop planted symlinks.","If the external target is legitimately needed, copy its contents into the repo rather than linking out."],"exampleFix":"// before\n# repo/asset -> /etc\nsrc, resolved = _safe_repo_source(repo, 'asset', label='task asset')\n// after\nlink = repo / 'asset'\nif link.is_symlink():\n    link.unlink()\nshutil.copytree('/trusted/asset', link)\nsrc, resolved = _safe_repo_source(repo, 'asset', label='task asset')","handlingStrategy":"validation","validationCode":"from pathlib import Path, PurePosixPath\n\ndef stays_within_repo(repo: Path, rel: str) -> bool:\n    candidate = PurePosixPath(rel)\n    if candidate.is_absolute() or '..' in candidate.parts or not candidate.parts:\n        return False\n    try:\n        (repo / Path(*candidate.parts)).resolve().relative_to(repo)\n        return True\n    except ValueError:\n        return False\n\nassert stays_within_repo(repo, relative)","typeGuard":"from pathlib import Path, PurePosixPath\n\ndef resolves_within_repo(repo: Path, value: object) -> bool:\n    if not isinstance(value, str):\n        return False\n    candidate = PurePosixPath(value)\n    if candidate.is_absolute() or '..' in candidate.parts or not candidate.parts:\n        return False\n    try:\n        (repo / Path(*candidate.parts)).resolve().relative_to(repo)\n        return True\n    except ValueError:\n        return False","tryCatchPattern":"try:\n    src, resolved = _safe_repo_source(repo, relative, label=label)\nexcept SandboxError as exc:\n    if 'escapes its allowed repository root' in str(exc):\n        link = repo / relative\n        if link.is_symlink():\n            link.unlink()\n            shutil.copytree(resolve_external_target(), link)\n        src, resolved = _safe_repo_source(repo, relative, label=label)\n    raise","preventionTips":["Scan the repo for escaping symlinks before staging.","Re-clone from trusted sources to drop planted links.","Copy external assets into the repo instead of symlinking.","Validate resolved paths stay within repo at input time."],"tags":["path-traversal","symlink","security","validation","sandbox"],"backgroundTag":null,"analyzedSha":"d540b00184d71a896261ee02670da9a92d59d8f7","analyzedAt":"2026-08-12T19:50:25.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}