abi/screenshot-to-code · error · HTTPException
Invalid asset path
Error message
Invalid asset path
What it means
Raised by GET /agent-runs/{run_id}/assets/{filename} (400) when the resolved, realpath'd candidate does not stay inside the run's assets directory. Because os.path.basename() already strips any '..'/slash components from filename, plain traversal strings never reach this branch in practice; it fires when realpath escapes the assets dir — i.e. a symlink inside assets/ pointing outside it. It is a defense-in-depth containment check.
Source
Thrown at backend/routes/agent_runs.py:222
_fetch_run(run_id)
run_dir = os.path.join(get_agent_runs_directory(), run_id)
for candidate in ("final_selfcontained.html", "final.html"):
path = os.path.join(run_dir, candidate)
if os.path.isfile(path):
with open(path, "r", encoding="utf-8") as f:
return HTMLResponse(content=f.read())
raise HTTPException(status_code=404, detail="Run has no captured output")
@router.get("/agent-runs/{run_id}/assets/{filename}")
async def get_agent_run_asset(run_id: str, filename: str) -> FileResponse:
_fetch_run(run_id)
assets_dir = os.path.join(get_agent_runs_directory(), run_id, "assets")
# basename() strips any traversal components before the containment check.
safe_name = os.path.basename(filename)
path = os.path.realpath(os.path.join(assets_dir, safe_name))
if not path.startswith(os.path.realpath(assets_dir) + os.sep):
raise HTTPException(status_code=400, detail="Invalid asset path")
if not os.path.isfile(path):
raise HTTPException(status_code=404, detail="Asset not found")
return FileResponse(path)
@router.post("/agent-runs/prune", response_model=PruneAgentRunsResponse)
async def prune_agent_runs(request: PruneAgentRunsRequest) -> PruneAgentRunsResponse:
if request.max_age_days < 1:
raise HTTPException(status_code=400, detail="max_age_days must be >= 1")
runs_directory = get_agent_runs_directory()
if not os.path.isdir(runs_directory):
return PruneAgentRunsResponse(deleted_count=0, freed_bytes=0)
cutoff_timestamp = (
datetime.now() - timedelta(days=request.max_age_days)
).timestamp()
View on GitHub (pinned to d026163f58)
Solutions
- Inspect assets/ under the run directory for symlinks (find <run_dir>/assets -type l) and replace them with regular files.
- Request only plain file names produced by the run's asset manifest.
- If you control asset writing, ensure it copies files rather than symlinking.
- Treat occurrences as a security signal — audit how the symlink got there.
Example fix
# before: assets/logo.png -> symlink to /etc/hostname GET /agent-runs/run_.../assets/logo.png # 400 'Invalid asset path' # after: replace the symlink with a real file rm <runs_dir>/<run_id>/assets/logo.png cp /real/path/logo.png <runs_dir>/<run_id>/assets/logo.png GET /agent-runs/run_.../assets/logo.png # 200
Defensive patterns
Strategy: validation
Validate before calling
import os
def is_plain_asset_filename(filename: str) -> bool:
"""Bare name, no separators — keeps you on the basename() happy path."""
return filename == os.path.basename(filename) and filename not in ("", ".", "..") Type guard
def is_safe_asset_name(filename: str) -> bool:
return (
isinstance(filename, str)
and filename == os.path.basename(filename)
and not filename.startswith(".")
) Try / catch
try:
resp = client.get(f"/agent-runs/{run_id}/assets/{filename}")
resp.raise_for_status()
except httpx.HTTPStatusError as e:
if e.response.status_code == 400 and "Invalid asset path" in e.response.text:
raise RuntimeError(f"symlink escape in assets dir for {filename!r}") from e
raise Prevention
- Never create symlinks inside a run's assets/ directory.
- Send bare filenames only — no directories, no '..'.
- Audit assets dirs for symlinks (find -type l) if this error appears.
When it happens
Trigger: Requesting an asset whose name matches a symlink stored inside assets/ that resolves to a file outside the assets directory. Path strings like ../../etc/passwd are neutralized by basename() and instead yield 'Asset not found' (404).
Common situations: A generation step or user created symlinks in the run's assets folder (e.g. to save disk space); compromised or hand-crafted assets directories; test suites probing traversal behavior with symlinks.
Related errors
- Invalid run id
- Invalid image path: {filename!r}
- max_age_days must be >= 1
- Invalid eval set name: {set_name!r}
- Invalid report filename
AI-assisted analysis of abi/screenshot-to-code@d026163f58 (2026-08-14).
Data as JSON: /api/errors/18a8fa2c300cdb30.
Report an issue: GitHub.