{"record":{"id":"e97f0ef8be1be243","repo":"666ghj/MiroFish","slug":"output-path-escaped-the-workspace","errorCode":null,"errorMessage":"output path escaped the workspace","messagePattern":"output path escaped the workspace","errorType":"exception","errorClass":"StarHistoryError","httpStatus":null,"severity":"critical","filePath":"scripts/star_history.py","lineNumber":467,"sourceCode":"    return (\n        json.dumps(state, ensure_ascii=False, indent=2, sort_keys=True) + \"\\n\"\n    ).encode(\"utf-8\")\n\n\ndef _safe_workspace(workspace: Path) -> Path:\n    try:\n        root = workspace.resolve(strict=True)\n    except OSError as exc:\n        raise StarHistoryError(\"workspace does not exist\") from exc\n    if not root.is_dir():\n        raise StarHistoryError(\"workspace is not a directory\")\n    return root\n\n\ndef _safe_target(workspace: Path, relative: Path, create_parent: bool) -> Path:\n    root = _safe_workspace(workspace)\n    if relative.is_absolute() or \"..\" in relative.parts:\n        raise StarHistoryError(\"output path escaped the workspace\")\n\n    current = root\n    for part in relative.parts[:-1]:\n        current = current / part\n        if current.is_symlink():\n            raise StarHistoryError(\"output directory cannot be a symbolic link\")\n    target = root / relative\n    if target.is_symlink():\n        raise StarHistoryError(\"output file cannot be a symbolic link\")\n    if create_parent:\n        try:\n            target.parent.mkdir(parents=True, exist_ok=True)\n        except OSError as exc:\n            raise StarHistoryError(\"could not create output directory\") from exc\n        current = root\n        for part in relative.parts[:-1]:\n            current = current / part\n            if current.is_symlink():","sourceCodeStart":449,"sourceCodeEnd":485,"githubUrl":"https://github.com/666ghj/MiroFish/blob/b5b53acc57189a4a42e44a23e149dc655c98fe82/scripts/star_history.py#L449-L485","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# before\nrelative = Path(user_supplied_name)  # may be \"../../etc/cron.d/x\"\n# after\nname = user_supplied_name.replace(\"/\", \"_\").replace(\"\\\\\", \"_\")\nif \"..\" in name:\n    raise ValueError(\"unsafe output name\")\nrelative = Path(name)","handlingStrategy":"validation","validationCode":"rel = Path(relative)\nif rel.is_absolute() or \"..\" in rel.parts:\n    raise ValueError(\"output path escaped the workspace\")","typeGuard":"from pathlib import Path\n\ndef is_safe_relative_target(relative: Path) -> bool:\n    p = Path(relative)\n    return not p.is_absolute() and \"..\" not in p.parts","tryCatchPattern":"try:\n    write_output(workspace, relative, data)\nexcept StarHistoryError as exc:\n    if \"output path escaped the workspace\" in str(exc):\n        # log the rejected path and the sanitized replacement; never retry as-is\n        ...","preventionTips":["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"],"tags":["security","path-traversal","filesystem"],"backgroundTag":null,"analyzedSha":"b5b53acc57189a4a42e44a23e149dc655c98fe82","analyzedAt":"2026-08-14T22:29:33.146Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}