666ghj/MiroFish · critical · StarHistoryError
output path escaped the workspace
Error message
output path escaped the workspace
What it means
Raised by _safe_target() when the requested relative output path is absolute or contains a '..' component, and separately (same message, scripts/star_history.py:493) when the resolved parent of the target does not stay under the workspace root. It is a path-traversal guard ensuring output files remain confined to the workspace.
Source
Thrown at scripts/star_history.py:467
return (
json.dumps(state, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
).encode("utf-8")
def _safe_workspace(workspace: Path) -> Path:
try:
root = workspace.resolve(strict=True)
except OSError as exc:
raise StarHistoryError("workspace does not exist") from exc
if not root.is_dir():
raise StarHistoryError("workspace is not a directory")
return root
def _safe_target(workspace: Path, relative: Path, create_parent: bool) -> Path:
root = _safe_workspace(workspace)
if relative.is_absolute() or ".." in relative.parts:
raise StarHistoryError("output path escaped the workspace")
current = root
for part in relative.parts[:-1]:
current = current / part
if current.is_symlink():
raise StarHistoryError("output directory cannot be a symbolic link")
target = root / relative
if target.is_symlink():
raise StarHistoryError("output file cannot be a symbolic link")
if create_parent:
try:
target.parent.mkdir(parents=True, exist_ok=True)
except OSError as exc:
raise StarHistoryError("could not create output directory") from exc
current = root
for part in relative.parts[:-1]:
current = current / part
if current.is_symlink():View on GitHub (pinned to b5b53acc57)
Solutions
- Pass a plain relative filename (no leading '/', no '..') as the output target
- Sanitize externally derived filenames: strip path separators and reject '..'
- Remove or relocate symlinks inside the workspace that point outside it
- If you genuinely need an output elsewhere, change the workspace configuration rather than escaping it
Example fix
# before
relative = Path(user_supplied_name) # may be "../../etc/cron.d/x"
# after
name = user_supplied_name.replace("/", "_").replace("\\", "_")
if ".." in name:
raise ValueError("unsafe output name")
relative = Path(name) Defensive patterns
Strategy: validation
Validate before calling
rel = Path(relative)
if rel.is_absolute() or ".." in rel.parts:
raise ValueError("output path escaped the workspace") Type guard
from pathlib import Path
def is_safe_relative_target(relative: Path) -> bool:
p = Path(relative)
return not p.is_absolute() and ".." not in p.parts Try / catch
try:
write_output(workspace, relative, data)
except StarHistoryError as exc:
if "output path escaped the workspace" in str(exc):
# log the rejected path and the sanitized replacement; never retry as-is
... Prevention
- Sanitize any user-supplied filename: strip separators and reject '..'
- Keep output names to a fixed allowlist or slugified values
- Never retry an escaped path unchanged; treat it as hostile input
- Avoid symlinks inside the workspace that point outside it
When it happens
Trigger: Passing an output relative path like '../../etc/passwd', '/etc/cron.d/x', or a symlinked directory whose resolution escapes the workspace root. Also triggered via the second site when target.parent.resolve() cannot be made relative to the resolved root.
Common situations: User- or config-supplied output filenames containing '..' or leading slashes; output names built from unvalidated external input (issue titles, repo names); symlinks inside the workspace pointing outside it.
Related errors
- output directory cannot be a symbolic link
- output file cannot be a symbolic link
- GitHub API redirect was refused
- workspace does not exist
- workspace is not a directory
AI-assisted analysis of 666ghj/MiroFish@b5b53acc57 (2026-08-14).
Data as JSON: /api/errors/e97f0ef8be1be243.
Report an issue: GitHub.