{"record":{"id":"93ffea946ea8bc38","repo":"Hmbown/CodeWhale","slug":"external-credential-path-must-be-absolute-and-lexically","errorCode":null,"errorMessage":"external credential path must be absolute and lexically normalized","messagePattern":"external credential path must be absolute and lexically normalized","errorType":"validation","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/external_credentials.rs","lineNumber":262,"sourceCode":"\n#[cfg(windows)]\nfn open_secure_regular_file(path: &Path, require_owner_only: bool) -> io::Result<File> {\n    use std::ffi::OsString;\n    use std::os::windows::ffi::OsStringExt;\n    use std::os::windows::fs::{MetadataExt, OpenOptionsExt};\n    use std::os::windows::io::AsRawHandle;\n    use std::path::Component;\n    use windows_sys::Win32::Storage::FileSystem::{\n        FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_OPEN_REPARSE_POINT, FILE_NAME_OPENED,\n        GetFinalPathNameByHandleW, VOLUME_NAME_DOS,\n    };\n\n    if !path.is_absolute()\n        || path\n            .components()\n            .any(|component| matches!(component, Component::CurDir | Component::ParentDir))\n    {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidInput,\n            \"external credential path must be absolute and lexically normalized\",\n        ));\n    }\n\n    // Reject every reparse-point component before the final open. The final\n    // handle is opened as the reparse point itself, checked again, and its\n    // kernel-resolved path is compared below. A second component pass catches\n    // replacement during the open window.\n    reject_windows_reparse_components(path)?;\n    let file = std::fs::OpenOptions::new()\n        .read(true)\n        .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)\n        .open(path)?;\n    let metadata = file.metadata()?;\n    if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0\n        || !metadata.file_type().is_file()\n    {","sourceCodeStart":244,"sourceCodeEnd":280,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/external_credentials.rs#L244-L280","documentation":"Before opening, the library validates the credential path lexically: it must be absolute and contain no `.` or `..` components. This throws InvalidInput for relative or non-normalized paths, closing trivial path-traversal and CWD-dependent redirection of the credential location.","triggerScenarios":"read_to_string / read_codewhale_owned_to_string is called with a relative path (e.g. \"token.json\", \"./creds/token\"), or a path containing \"..\" (e.g. \"/home/me/../root/creds\"), or a trailing/interior \".\" component.","commonSituations":"Config derives the path by string-concatenation with \"..\" instead of using path joins; a CLI flag takes a relative path and is passed through unmodified; environment-based path built at runtime relative to a working directory.","solutions":["Convert to an absolute path before the call: `std::fs::canonicalize` or anchor against a known base dir.","Remove `.` and `..` by building paths with `PathBuf::join`/`push` instead of string concatenation.","If the base directory itself may be relative, resolve it once at startup and store the absolute, normalized result.","Fix the config/env value to a full absolute path."],"exampleFix":"// before\nlet path = PathBuf::from(format!(\"/home/me/.codewhale/../.codewhale/{}\", name));\n// after\nlet path = std::fs::canonicalize(PathBuf::from(\"/home/me/.codewhale\").join(name))?;","handlingStrategy":"validation","validationCode":"fn ensure_absolute_normalized(path: &Path) -> std::io::Result<()> {\n    use std::path::Component;\n    if !path.is_absolute()\n        || path.components().any(|c| matches!(c, Component::CurDir | Component::ParentDir))\n    {\n        return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, \"path must be absolute and normalized\"));\n    }\n    Ok(())\n}","typeGuard":"fn is_absolute_normalized(path: &Path) -> bool {\n    use std::path::Component;\n    path.is_absolute()\n        && !path.components().any(|c| matches!(c, Component::CurDir | Component::ParentDir))\n}","tryCatchPattern":"let abs = std::fs::canonicalize(raw_path)\n    .map_err(|e| eprintln!(\"cannot resolve credential path {raw:?}: {e}\"))?;\nmatch read_codewhale_owned_to_string(&abs) {\n    Ok(creds) => use(creds),\n    Err(e) => return Err(e),\n}","preventionTips":["Always canonicalize() user-supplied paths before storing or passing them.","Build paths with PathBuf::join, never string concatenation with \"..\".","Anchor relative config values against a fixed base directory at startup.","Validate configured paths once at load time and fail fast."],"tags":["io","security","path-validation","credentials"],"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-22T06:17:15.046Z"}