{"record":{"id":"ecdfd43257ac4bb6","repo":"can1357/oh-my-pi","slug":"invalid-workspace-key-workspace-key-r","errorCode":null,"errorMessage":"invalid workspace_key {workspace_key!r}","messagePattern":"invalid workspace_key (.+?)","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"python/robomp/src/proxy/server.py","lineNumber":218,"sourceCode":"        if start_side is not None:\n            start_side_str = _require_str(start_side, f\"comments[{idx}].start_side\")\n            if start_side_str not in (\"RIGHT\", \"LEFT\"):\n                raise HTTPException(400, f\"comments[{idx}].start_side must be RIGHT or LEFT\")\n            comment[\"start_side\"] = start_side_str\n        comments.append(comment)\n    return comments\n\n\ndef _pool_dir(cfg: Settings, repo: str) -> Path:\n    _validate_repo_name(repo)\n    return Path(cfg.workspace_root) / \"_pool\" / repo.replace(\"/\", \"__\")\n\n\ndef _workspace_repo_dir(cfg: Settings, workspace_key: str) -> Path:\n    # Defense-in-depth: workspace_key is constructed by `sandbox.workspace_key`\n    # as `<repo_with_underscores>__<number>`. Reject anything outside that shape.\n    if \"/\" in workspace_key or workspace_key.startswith(\".\") or \"..\" in workspace_key:\n        raise HTTPException(400, f\"invalid workspace_key {workspace_key!r}\")\n    return Path(cfg.workspace_root) / workspace_key / \"repo\"\n\n\ndef _resolve_token(cfg: Settings) -> str:\n    if cfg.github_token is None:\n        # Will already have been caught at startup, but stay defensive.\n        raise HTTPException(500, \"gh-proxy: GITHUB_TOKEN not configured\")\n    return cfg.github_token.get_secret_value()\n\n\ndef _resolve_hmac_key(cfg: Settings) -> bytes:\n    if cfg.gh_proxy_hmac_key is None:\n        raise HTTPException(500, \"gh-proxy: ROBOMP_GH_PROXY_HMAC_KEY not configured\")\n    return cfg.gh_proxy_hmac_key.get_secret_value().encode(\"utf-8\")\n\n\n_ORIGIN_READ_TIMEOUT_SECONDS = 5.0\n","sourceCodeStart":200,"sourceCodeEnd":236,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/python/robomp/src/proxy/server.py#L200-L236","documentation":"`_workspace_repo_dir` treats workspace_key as untrusted path input and enforces the shape `<repo_with_underscores>__<number>`. Keys containing '/', leading '.', or '..' are rejected with this HTTP 400 to prevent path traversal outside cfg.workspace_root.","triggerScenarios":"Calling git_push_endpoint or git_push_release_endpoint with a workspace_key containing slashes (e.g. \"owner/repo__1\"), starting with '.', or containing '..' segments.","commonSituations":"Clients building workspace_key from the raw 'owner/repo' name instead of the underscore-flattened repo id; relative-path fragments leaking in from shell variables; tampering attempts during auth-bypass probing.","solutions":["Construct workspace_key via the sandbox.workspace_key helper — repo with '/' replaced by underscores plus '__<number>' (e.g. owner_repo__1).","Strip or replace '/' and leading dots from the repo component before deriving the key client-side.","Use a workspace_key previously returned by the sandbox/session API rather than hand-building one.","If a legitimate key is being rejected, check for hidden whitespace or encoding artifacts adding '..' segments."],"exampleFix":"// before\nkey = f\"{owner}/{repo}__1\"\n// after\nkey = f\"{repo.replace('/', '_')}__1\"  # e.g. sandbox.workspace_key(repo, 1)","handlingStrategy":"validation","validationCode":"import re\nWORKSPACE_KEY_RE = re.compile(r\"^[^/.]+__\\d+$\")\nif not WORKSPACE_KEY_RE.fullmatch(workspace_key):\n    raise ValueError(f\"workspace_key {workspace_key!r} must be '<repo>__<n>' with no slashes/dots\")","typeGuard":"def is_workspace_key(v: object) -> TypeGuard[str]:\n    return (\n        isinstance(v, str)\n        and \"/\" not in v\n        and not v.startswith(\".\")\n        and \"..\" not in v\n        and \"__\" in v\n        and v.rsplit(\"__\", 1)[1].isdigit()\n    )","tryCatchPattern":"try:\n    resp = http.post(f\"{base}/git/push\", json=payload)\n    resp.raise_for_status()\nexcept httpx.HTTPStatusError as e:\n    if e.response.status_code == 400 and \"invalid workspace_key\" in e.response.text:\n        raise ValueError(\"workspace_key must come from sandbox.workspace_key(repo, n)\") from e\n    raise","preventionTips":["Always derive workspace_key from the sandbox helper, never from raw owner/repo strings","Replace '/' with '_' in the repo component before composing the key","Treat workspace_key as opaque — store and echo it, don't reconstruct it","Add a client-side regex check mirroring the server's shape before each push"],"tags":["http-400","path-traversal","security","validation"],"backgroundTag":"path-traversal-rejected","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}