Hmbown/CodeWhale · error · io::Error
external credential path must be absolute and lexically…
Error message
external credential path must be absolute and lexically normalized
What it means
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.
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.
Example fix
// before
let path = PathBuf::from(format!("/home/me/.codewhale/../.codewhale/{}", name));
// after
let path = std::fs::canonicalize(PathBuf::from("/home/me/.codewhale").join(name))?; Defensive patterns
Strategy: validation
Validate before calling
fn ensure_absolute_normalized(path: &Path) -> std::io::Result<()> {
use std::path::Component;
if !path.is_absolute()
|| path.components().any(|c| matches!(c, Component::CurDir | Component::ParentDir))
{
return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "path must be absolute and normalized"));
}
Ok(())
} Type guard
fn is_absolute_normalized(path: &Path) -> bool {
use std::path::Component;
path.is_absolute()
&& !path.components().any(|c| matches!(c, Component::CurDir | Component::ParentDir))
} Try / catch
let abs = std::fs::canonicalize(raw_path)
.map_err(|e| eprintln!("cannot resolve credential path {raw:?}: {e}"))?;
match read_codewhale_owned_to_string(&abs) {
Ok(creds) => use(creds),
Err(e) => return Err(e),
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- credential handoff could not write to stdout
- external credential path must be absolute
- external credential path must name a regular file
- InvalidInput
- audited skill path does not match owned package
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/93ffea946ea8bc38.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/external_credentials.rs:262
#[cfg(windows)]
fn open_secure_regular_file(path: &Path, require_owner_only: bool) -> io::Result<File> {
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
use std::os::windows::fs::{MetadataExt, OpenOptionsExt};
use std::os::windows::io::AsRawHandle;
use std::path::Component;
use windows_sys::Win32::Storage::FileSystem::{
FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_OPEN_REPARSE_POINT, FILE_NAME_OPENED,
GetFinalPathNameByHandleW, VOLUME_NAME_DOS,
};
if !path.is_absolute()
|| path
.components()
.any(|component| matches!(component, Component::CurDir | Component::ParentDir))
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"external credential path must be absolute and lexically normalized",
));
}
// Reject every reparse-point component before the final open. The final
// handle is opened as the reparse point itself, checked again, and its
// kernel-resolved path is compared below. A second component pass catches
// replacement during the open window.
reject_windows_reparse_components(path)?;
let file = std::fs::OpenOptions::new()
.read(true)
.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)?;
let metadata = file.metadata()?;
if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
|| !metadata.file_type().is_file()
{View on GitHub (pinned to 73e0f67d83)