abhigyanpatwari/GitNexus · error · SandboxError
{label} escapes its allowed repository root: {relative}
Error message
{label} escapes its allowed repository root: {relative} What it means
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.
Source
Thrown at eval/workflow_bench/proposer_sandbox.py:570
try:
resolved = lexical.resolve(strict=True)
except OSError as exc:
raise SandboxError(f"{label} must be a real directory: {lexical}: {exc}") from exc
if resolved != lexical:
raise SandboxError(f"{label} must not traverse symlinks: {lexical}")
return lexical
def _safe_repo_source(repo: Path, relative: str, *, label: str) -> tuple[Path, Path]:
candidate = PurePosixPath(relative)
if candidate.is_absolute() or ".." in candidate.parts or not candidate.parts:
raise SandboxError(f"{label} must be a repository-relative path: {relative!r}")
lexical = repo / Path(*candidate.parts)
resolved = lexical.resolve()
try:
resolved.relative_to(repo)
except ValueError as exc:
raise SandboxError(f"{label} escapes its allowed repository root: {relative}") from exc
if not resolved.exists():
raise SandboxError(f"{label} does not exist: {relative}")
return lexical, resolved
def _prepare_clone_target(
clone: Path,
relative: PurePosixPath,
*,
directory: bool | None,
label: str,
) -> Path:
"""Validate/create a clone-local target without following any symlink.
This runs before Bubblewrap, so ordinary ``Path.mkdir``/``touch`` calls
are not acceptable: an untrusted tracked parent symlink could redirect a
mount placeholder write into the host filesystem.
"""View on GitHub (pinned to d540b00184)
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.
Example fix
// before
# repo/asset -> /etc
src, resolved = _safe_repo_source(repo, 'asset', label='task asset')
// after
link = repo / 'asset'
if link.is_symlink():
link.unlink()
shutil.copytree('/trusted/asset', link)
src, resolved = _safe_repo_source(repo, 'asset', label='task asset') Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path, PurePosixPath
def stays_within_repo(repo: Path, rel: str) -> bool:
candidate = PurePosixPath(rel)
if candidate.is_absolute() or '..' in candidate.parts or not candidate.parts:
return False
try:
(repo / Path(*candidate.parts)).resolve().relative_to(repo)
return True
except ValueError:
return False
assert stays_within_repo(repo, relative) Type guard
from pathlib import Path, PurePosixPath
def resolves_within_repo(repo: Path, value: object) -> bool:
if not isinstance(value, str):
return False
candidate = PurePosixPath(value)
if candidate.is_absolute() or '..' in candidate.parts or not candidate.parts:
return False
try:
(repo / Path(*candidate.parts)).resolve().relative_to(repo)
return True
except ValueError:
return False Try / catch
try:
src, resolved = _safe_repo_source(repo, relative, label=label)
except SandboxError as exc:
if 'escapes its allowed repository root' in str(exc):
link = repo / relative
if link.is_symlink():
link.unlink()
shutil.copytree(resolve_external_target(), link)
src, resolved = _safe_repo_source(repo, relative, label=label)
raise Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- {label} must be a real non-symlink directory: {path}
- {label} contains an unsafe path component: {relative}
- {label} must be a regular non-symlink file: {path}
- unsafe results artifact path: {relative_value!r}
- results artifact parent must be a real directory: {current}
AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12).
Data as JSON: /api/errors/aa0dfa187ae457dd.
Report an issue: GitHub.