{"record":{"id":"e3e8ac7e3267e493","repo":"shareAI-lab/learn-claude-code","slug":"path-escapes-workspace-p-e3e8ac","errorCode":null,"errorMessage":"Path escapes workspace: {p}","messagePattern":"Path escapes workspace: (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"s02_tool_use/code.py","lineNumber":74,"sourceCode":"        return \"Error: Dangerous command blocked\"\n    try:\n        r = subprocess.run(command, shell=True, cwd=WORKDIR,\n                           capture_output=True, text=True,\n                           encoding=\"utf-8\", errors=\"replace\", timeout=120)\n        out = (r.stdout + r.stderr).strip()\n        return out[:50000] if out else \"(no output)\"\n    except subprocess.TimeoutExpired:\n        return \"Error: Timeout (120s)\"\n    except (FileNotFoundError, OSError) as e:\n        return f\"Error: {e}\"\n\n\n# -- New in s02: four tools --\n\ndef safe_path(p: str) -> Path:\n    path = (WORKDIR / p).resolve()\n    if not path.is_relative_to(WORKDIR):\n        raise ValueError(f\"Path escapes workspace: {p}\")\n    return path\n\n\ndef run_read(path: str, limit: int | None = None) -> str:\n    try:\n        lines = safe_path(path).read_text().splitlines()\n        if limit and limit < len(lines):\n            lines = lines[:limit] + [f\"... ({len(lines) - limit} more lines)\"]\n        return \"\\n\".join(lines)\n    except Exception as e:\n        return f\"Error: {e}\"\n\n\ndef run_write(path: str, content: str) -> str:\n    try:\n        file_path = safe_path(path)\n        file_path.parent.mkdir(parents=True, exist_ok=True)\n        file_path.write_text(content)","sourceCodeStart":56,"sourceCodeEnd":92,"githubUrl":"https://github.com/shareAI-lab/learn-claude-code/blob/985456f4adea6f4df8fbad4112245dbd97444eae/s02_tool_use/code.py#L56-L92","documentation":"The s02 session's copy of safe_path(): it joins a relative path onto WORKDIR, resolves it, and raises this ValueError when the result falls outside WORKDIR. It is the shared gate for run_read and the other file tools introduced in that session. Because .resolve() follows symlinks, both explicit ../ traversal and symlink-based escapes are caught.","triggerScenarios":"run_read(\"../../etc/passwd\"); run_read(\"/etc/hosts\") since joining an absolute path discards WORKDIR; a path stepping through a symlink inside WORKDIR that points to /usr or another outside location.","commonSituations":"Agents passing absolute paths echoed by earlier tool output; workspaces with vendored symlinked dependencies resolving outside the tree; WORKDIR and actual file location disagreeing after a project move.","solutions":["Express every path relative to WORKDIR with no .. components","Convert legitimate absolute paths via Path(p).resolve().relative_to(WORKDIR.resolve())","Remove or relocate symlinks inside WORKDIR that point outside it","Confirm WORKDIR matches the project root the agent believes it is in"],"exampleFix":"// before\nrun_read(\"../shared/config.yaml\")\n// after\nrun_read(\"shared/config.yaml\")  # assuming shared/ lives under WORKDIR","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef safe_rel(p: str) -> str:\n    cand = Path(p)\n    if not cand.is_absolute():\n        cand = WORKDIR / cand\n    cand = cand.resolve()\n    assert cand.is_relative_to(WORKDIR.resolve()), f\"{p} escapes workspace\"\n    return str(cand.relative_to(WORKDIR.resolve()))\n\nrun_read(safe_rel(user_path))","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Pass only relative paths without '..' segments","Do not feed tool-echoed absolute paths back into file tools","Resolve WORKDIR once at startup and use the real path"],"tags":["security","path-traversal","sandbox","validation"],"backgroundTag":null,"analyzedSha":"985456f4adea6f4df8fbad4112245dbd97444eae","analyzedAt":"2026-08-14T22:02:26.028Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}