{"record":{"id":"6b1eba7a7b7d3551","repo":"Hmbown/CodeWhale","slug":"codewhale-owned-oauth-path-escaped-the-credentials-directory","errorCode":null,"errorMessage":"Codewhale-owned OAuth path escaped the credentials directory","messagePattern":"Codewhale-owned OAuth path escaped the credentials directory","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/oauth.rs","lineNumber":2200,"sourceCode":"        bail!(\n            \"Codewhale-owned {} OAuth credentials are not configured\",\n            oauth_provider_params(provider).display_name\n        );\n    };\n    let name = path\n        .file_name()\n        .and_then(|name| name.to_str())\n        .context(\"Codewhale-owned OAuth path must have a UTF-8 basename\")?;\n    codewhale_config::with_xai_oauth_lifecycle_lock(|store| {\n        get_owned_credentials_locked(provider, store, name, |issuer, client_id, refresh| {\n            refresh_for_provider(provider, client, issuer, client_id, refresh)\n        })\n    })\n}\n\nfn get_owned_credentials_at(provider: OAuthProvider, path: &Path) -> Result<OwnedOAuthCredentials> {\n    let directory = codewhale_config::xai_oauth_credentials_dir()?;\n    anyhow::ensure!(\n        path.parent() == Some(directory.as_path()),\n        \"Codewhale-owned OAuth path escaped the credentials directory\"\n    );\n    let name = path\n        .file_name()\n        .and_then(|name| name.to_str())\n        .context(\"Codewhale-owned OAuth path must have a UTF-8 basename\")?;\n    anyhow::ensure!(\n        name == provider.legacy_file_name() || provider.is_valid_generation(name),\n        \"Codewhale-owned OAuth path has an invalid basename\"\n    );\n    codewhale_config::with_xai_oauth_lifecycle_lock(|store| {\n        get_owned_credentials_locked(provider, store, name, |issuer, client_id, refresh| {\n            refresh_for_provider(\n                provider,\n                &ReqwestOAuthFormClient,\n                issuer,\n                client_id,","sourceCodeStart":2182,"sourceCodeEnd":2218,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/oauth.rs#L2182-L2218","documentation":"get_owned_credentials_at validates that the requested credential file's parent directory is exactly the Codewhale xAI OAuth credentials directory (xai_oauth_credentials_dir). Any other parent — including subdirectories or sibling paths — is rejected as a path-traversal guard for Codewhale-owned storage.","triggerScenarios":"Calling get_owned_credentials_at(provider, path) where path.parent() != xai_oauth_credentials_dir(), e.g. a path built from user input, \"../\", or a custom location.","commonSituations":"Scripting the credentials path with an env override or relative path; migrating an owned file manually to another directory; symlinked credentials directory resolving elsewhere.","solutions":["Use the library's own storage APIs (store.path_for / xai_oauth_credentials_dir) instead of constructing paths manually.","Remove any \"..\" components or custom directories from the requested path.","Check that the credentials directory isn't a symlink pointing outside the expected location."],"exampleFix":"// before\nlet path = PathBuf::from(\"~/.codewhale/other/xai.json\");\n// after\nlet dir = codewhale_config::xai_oauth_credentials_dir()?;\nlet path = dir.join(provider.legacy_file_name());","handlingStrategy":"validation","validationCode":"if (path.dirname(requested) !== codewhale.xaiOauthCredentialsDir()) throw new Error('use store.path_for');","typeGuard":"const inOwnedDir = (p, dir) => path.dirname(path.resolve(p)) === path.resolve(dir);","tryCatchPattern":"try { loadOwnedAt(p); } catch (e) { if (String(e).includes('escaped the credentials directory')) loadOwnedViaStore(); }","preventionTips":["Always derive credential paths from store.path_for, never from user input","Avoid symlinking the credentials directory elsewhere","Reject any path containing \"..\" before passing it to owned-storage APIs"],"tags":["oauth","path-traversal","security"],"backgroundTag":"path-traversal-blocked","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}