{"record":{"id":"a410181a855d35cd","repo":"Hmbown/CodeWhale","slug":"external-credential-path-must-be-lexically-normalized","errorCode":null,"errorMessage":"external credential path must be lexically normalized","messagePattern":"external credential path must be lexically normalized","errorType":"validation","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/external_credentials.rs","lineNumber":173,"sourceCode":"    // SAFETY: `root` is a valid C string and flags require no variadic mode.\n    let root_fd = unsafe {\n        libc::open(\n            root.as_ptr(),\n            libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC,\n        )\n    };\n    if root_fd < 0 {\n        return Err(io::Error::last_os_error());\n    }\n    // SAFETY: `root_fd` is newly owned after the successful `open`.\n    let mut current = unsafe { File::from_raw_fd(root_fd) };\n    let mut normals = path\n        .components()\n        .filter_map(|component| match component {\n            Component::Normal(part) => Some(Ok(part)),\n            Component::RootDir => None,\n            Component::Prefix(_) | Component::CurDir | Component::ParentDir => {\n                Some(Err(io::Error::new(\n                    io::ErrorKind::InvalidInput,\n                    \"external credential path must be lexically normalized\",\n                )))\n            }\n        })\n        .peekable();\n\n    let mut opened_leaf = false;\n    while let Some(component) = normals.next() {\n        let component = component?;\n        let component = CString::new(component.as_bytes()).map_err(|_| {\n            io::Error::new(\n                io::ErrorKind::InvalidInput,\n                \"external credential path contains a NUL byte\",\n            )\n        })?;\n        let leaf = normals.peek().is_none();\n        #[cfg(test)]","sourceCodeStart":155,"sourceCodeEnd":191,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/external_credentials.rs#L155-L191","documentation":"During its component walk, `open_secure_regular_file` accepts only normal path components and the root. Components like `.` (CurDir), `..` (ParentDir), or Windows prefixes are rejected with this `InvalidInput` error, guaranteeing the path is lexically normalized before it is opened — no symlink-style or traversal-style relative navigation can occur inside the secure open.","triggerScenarios":"Calling `read_to_string`/`read_codewhale_owned_to_string` with a path containing `.`, `..`, or a non-Unix prefix component, e.g. `/home/me/../me/.config/creds` or `./credentials`.","commonSituations":"Paths assembled by string concatenation or templating that leave `..` segments in; user-supplied config values not canonicalized; migrated configs with symlink-heavy layouts baked in as `..` chains.","solutions":["Lexically normalize the path before passing it: remove `.` and resolve `..` against its base, or call `path_clean`/manual canonicalization of components.","If the target is behind symlinks you want resolved, canonicalize the intended real path yourself and pass the resulting absolute, normal path.","Fix the config value or code that produced the unnormalized path — write the final path directly.","Note `fs::canonicalize` alone may be inappropriate pre-open for security paths; prefer rejecting and reconstructing the path cleanly."],"exampleFix":"// before\nlet creds = read_to_string(\"/home/me/../me/.config/creds\")?;\n// after\nlet creds = read_to_string(\"/home/me/.config/creds\")?; // lexically normalized","handlingStrategy":"validation","validationCode":"fn is_lexically_normalized(p: &Path) -> bool {\n    use std::path::Component;\n    !p.components().any(|c| matches!(c,\n        Component::CurDir | Component::ParentDir | Component::Prefix(_)))\n}\nif !is_lexically_normalized(&path) {\n    return Err(anyhow::anyhow!(\"credential path not normalized: {path:?}\"));\n}","typeGuard":"fn is_safe_credential_path(p: &Path) -> bool {\n    use std::path::Component;\n    p.is_absolute() && p.components().all(|c| matches!(c, Component::RootDir | Component::Normal(_)))\n}","tryCatchPattern":"match read_to_string(&cred_path) {\n    Err(e) if e.to_string().contains(\"lexically normalized\") => {\n        eprintln!(\"Resolve '.'/'..' in {cred_path:?} before passing it (no traversal allowed).\");\n    }\n    other => other?,\n}","preventionTips":["Normalize paths (resolve `.` and `..` lexically) before storing them in config.","Never pass user-supplied relative traversal segments into credential loaders.","Prefer canonical, written-out absolute paths in configuration files.","Treat unnormalized paths as a red flag in untrusted input paths."],"tags":["security","filesystem","path-traversal","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"}