{"record":{"id":"9728161a9d8e75f2","repo":"odysseus-dev/odysseus","slug":"invalid-oauth-field-name-path-must-stay-under","errorCode":null,"errorMessage":"Invalid OAuth {field_name}: path must stay under {base}","messagePattern":"Invalid OAuth (.+?): path must stay under (.+?)","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"routes/mcp/mcp_routes.py","lineNumber":44,"sourceCode":"    return Path(MCP_OAUTH_DIR).resolve(strict=False)\n\n\ndef _resolve_mcp_oauth_path(raw_path, field_name: str) -> str:\n    \"\"\"Resolve an MCP OAuth path and keep it under DATA_DIR/mcp_oauth.\"\"\"\n    raw = str(raw_path or \"\").strip()\n    if not raw:\n        return \"\"\n\n    base = _mcp_oauth_base_dir()\n    path = Path(os.path.expanduser(raw))\n    if not path.is_absolute():\n        path = base / path\n    resolved = path.resolve(strict=False)\n\n    try:\n        resolved.relative_to(base)\n    except ValueError as exc:\n        raise HTTPException(\n            400,\n            f\"Invalid OAuth {field_name}: path must stay under {base}\",\n        ) from exc\n    return str(resolved)\n\n\ndef _sanitize_mcp_oauth_config(oauth_cfg):\n    \"\"\"Return an OAuth config copy with file paths confined to mcp_oauth.\"\"\"\n    if not oauth_cfg:\n        return oauth_cfg\n    if not isinstance(oauth_cfg, dict):\n        return {}\n    sanitized = dict(oauth_cfg)\n    for field_name in (\"keys_file\", \"token_file\"):\n        if sanitized.get(field_name):\n            sanitized[field_name] = _resolve_mcp_oauth_path(\n                sanitized[field_name],\n                field_name,","sourceCodeStart":26,"sourceCodeEnd":62,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/mcp/mcp_routes.py#L26-L62","documentation":"Raised when an OAuth file path (keys_file / token_file) supplied in an MCP server's oauth_file or oauth_config resolves outside the mcp_oauth base directory. The code expands ~, joins relative paths onto the base, resolves symlinks, then calls Path.relative_to(base); on ValueError it rejects the path with 400. This is a path-traversal containment guard for the OAuth credential store.","triggerScenarios":"POSTing/PATCHing an MCP server with oauth_file=\"/etc/google/keys.json\" (absolute path outside base), a relative path with traversal like \"../../secrets/keys.json\", or a symlink inside the base that resolves elsewhere.","commonSituations":"Reusing an absolute path from a different machine or setup guide; pointing at a keys file in the project root or home directory instead of the managed mcp_oauth directory; symlinked dotfiles whose realpath escapes the base.","solutions":["Place the keys/token files inside the directory returned by _mcp_oauth_base_dir() and reference them by bare filename or path relative to it.","If the file lives elsewhere, copy it into the mcp_oauth base dir and update the config to the relative name.","Verify with Path(p).resolve().relative_to(base) locally before submitting the config."],"exampleFix":"# before\noauth_file = \"/home/me/creds/client_secret.json\"\n\n# after\nimport shutil, pathlib\nbase = pathlib.Path(_mcp_oauth_base_dir())\nshutil.copy(\"/home/me/creds/client_secret.json\", base / \"client_secret.json\")\noauth_file = \"client_secret.json\"  # relative to base","handlingStrategy":"validation","validationCode":"from pathlib import Path\ndef oauth_path_ok(raw: str, base: Path) -> bool:\n    p = Path(raw).expanduser()\n    if not p.is_absolute():\n        p = base / p\n    try:\n        p.resolve(strict=False).relative_to(base.resolve(strict=False))\n        return True\n    except ValueError:\n        return False","typeGuard":null,"tryCatchPattern":"On 400 from server registration, check the detail for 'path must stay under' and fix the path rather than retrying.","preventionTips":["Keep all OAuth credentials in the managed mcp_oauth directory.","Never ship absolute credential paths in configs shared across machines.","Avoid symlinks under the base dir — resolve() follows them and can escape."],"tags":["security","path-traversal","oauth","mcp","filesystem"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}