{"record":{"id":"cda98eb44b89f19a","repo":"Hmbown/CodeWhale","slug":"xai-oauth-private-basename-must-be-one-utf-8-path","errorCode":null,"errorMessage":"xAI OAuth private basename must be one UTF-8 path component","messagePattern":"xAI OAuth private basename must be one UTF-8 path component","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/config/src/xai_credentials.rs","lineNumber":570,"sourceCode":"\nfn validate_owned_auth_name(name: &str) -> Result<()> {\n    anyhow::ensure!(\n        name == LEGACY_XAI_OAUTH_FILE_NAME\n            || name == LEGACY_CHATGPT_OAUTH_FILE_NAME\n            || is_valid_xai_oauth_generation(name)\n            || is_valid_chatgpt_oauth_generation(name),\n        \"invalid Codewhale-owned OAuth basename\"\n    );\n    Ok(())\n}\n\nfn is_chatgpt_owned_auth_name(name: &str) -> bool {\n    name == LEGACY_CHATGPT_OAUTH_FILE_NAME || is_valid_chatgpt_oauth_generation(name)\n}\n\nfn validate_private_basename(name: &str) -> Result<()> {\n    let path = Path::new(name);\n    anyhow::ensure!(\n        path.components().count() == 1\n            && matches!(path.components().next(), Some(Component::Normal(_)))\n            && path.file_name().and_then(|value| value.to_str()) == Some(name),\n        \"xAI OAuth private basename must be one UTF-8 path component\"\n    );\n    Ok(())\n}\n\n#[cfg(unix)]\nfn open_owned_credentials_directory(directory: &Path) -> Result<XaiOAuthCredentialStore> {\n    use std::os::fd::FromRawFd as _;\n    use std::os::unix::fs::{MetadataExt as _, PermissionsExt as _};\n\n    anyhow::ensure!(\n        directory.is_absolute(),\n        \"xAI OAuth credentials directory must be absolute\"\n    );\n    // SAFETY: the literal root path contains no interior NUL and the returned","sourceCodeStart":552,"sourceCodeEnd":588,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/config/src/xai_credentials.rs#L552-L588","documentation":"validate_private_basename requires the name to be exactly one normal, UTF-8 path component — no separators, no parent refs, and byte-identical to its own file_name. This blocks path traversal and non-UTF-8 tricks in low-level raw file operations.","triggerScenarios":"Calling open_at, remove_raw, rename_raw, or the internal/windows file openers with a name containing '/', '\\\\', '..', an interior NUL pattern, non-UTF-8 bytes, or any multi-component path.","commonSituations":"Passing a relative subpath like \"sub/dir/file.json\"; user-controlled input reaching the name parameter (path traversal attempt); platform path separators slipping in from concatenation code.","solutions":["Pass only a bare file name with no separators; build full paths via the store, not the caller.","Sanitize/validate user input before it reaches these APIs (reject any name containing a path separator).","Use path_for or validate_owned_auth_name first to map logical names to legal basenames.","If handling untrusted names, reject early in your own layer with a clear message."],"exampleFix":"// before\nstore.remove_raw(\"../escaped.json\")?; // traversal attempt\n// after\nlet name = \"xai.oauth.v2.retired.json\";\nassert_eq!(Path::new(name).components().count(), 1);\nstore.remove_raw(name)?;","handlingStrategy":"validation","validationCode":"use std::path::{Path, Component};\nfn safe_basename(name: &str) -> bool {\n    let p = Path::new(name);\n    p.components().count() == 1\n        && matches!(p.components().next(), Some(Component::Normal(_)))\n        && p.file_name().and_then(|f| f.to_str()) == Some(name)\n}","typeGuard":"fn is_single_normal_component(name: &str) -> bool {\n    !name.contains('/') && !name.contains('\\\\') && name != \"..\" && !name.is_empty()\n}","tryCatchPattern":null,"preventionTips":["Reject any user input containing path separators before it reaches the store","Treat file names as internal identifiers, not user-facing paths","Add a test asserting traversal strings are rejected","Only use the raw *_raw APIs with names produced by the store itself"],"tags":["path-traversal","security","validation","filename"],"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"}