Hmbown/CodeWhale · error · anyhow::Error

external credential path escapes its absolute root: {}

Error message

external credential path escapes its absolute root: {}

What it means

External-credential consent paths are normalized lexically (only '.'/'..' components, no canonicalize, so no symlink blessing and no I/O before consent) inside resolve_external_credential_path(). If a ParentDir ('..') component pops past the front of the accumulated absolute path, the path would escape its absolute root and normalization bails rather than producing a path outside the granted scope.

Source

Thrown at crates/config/src/external_credentials.rs:129

    } else {
        std::env::current_dir()
            .map_err(|err| anyhow::anyhow!("resolving external credential path: {err}"))?
            .join(path)
    };

    // Normalize only lexical `.` / `..` components. Canonicalization would
    // inspect a credential path before consent exists and would also silently
    // bless a symlink target. The secure reader rejects symlink/reparse-point
    // components when the granted capability is actually consumed.
    let mut normalized = PathBuf::new();
    for component in absolute.components() {
        match component {
            Component::Prefix(prefix) => normalized.push(prefix.as_os_str()),
            Component::RootDir => normalized.push(component.as_os_str()),
            Component::CurDir => {}
            Component::ParentDir => {
                if !normalized.pop() {
                    bail!(
                        "external credential path escapes its absolute root: {}",
                        quote_os_path(&absolute)
                    );
                }
            }
            Component::Normal(part) => normalized.push(part),
        }
    }
    if !normalized.is_absolute() {
        bail!(
            "external credential path must resolve to an absolute path: {}",
            quote_os_path(&normalized)
        );
    }
    Ok(normalized)
}

/// The side-effect envelope Codewhale may use for an external credential.

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Fix the consented path so it stays inside its root after removing '.'/'..' components
  2. Re-create the consent with the real absolute file location of the external credential

Example fix

# before
/path/to/keys/../../other/creds.env   # escapes during normalization

# after
/other/creds.env   # or the direct absolute path with no '..' segments
Defensive patterns

Strategy: validation

Validate before calling

// Reject traversal before creating/using a consent:
fn lexically_within_root(path: &std::path::Path) -> bool {
    let mut norm = std::path::PathBuf::new();
    for c in path.components() {
        match c {
            std::path::Component::ParentDir => if !norm.pop() { return false },
            std::path::Component::CurDir => {}
            other => norm.push(other.as_os_str()),
        }
    }
    norm.is_absolute()
}

Type guard

fn is_traversal_free(absolute: &Path) -> bool {
    let mut depth = 0usize;
    for c in absolute.components() {
        match c {
            Component::Normal(_) => depth += 1,
            Component::ParentDir => { if depth == 0 { return false } depth -= 1 }
            _ => {}
        }
    }
    true
}

Try / catch

match resolve_external_credential_path(&p) {
    Ok(normalized) => { /* use normalized */ }
    Err(e) if e.to_string().contains("escapes its absolute root") => {
        // input error in consent data: reject the consent, never coerce
        reject_consent_input(e)
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: A consented external credential path whose lexical normalization walks above the root: e.g. '/keys/../../etc/passwd' — after popping '/keys' then '/', another '..' finds nothing left to pop and the bail fires.

Common situations: Hand-crafted or copy-pasted consent paths with too many '..' segments; dotfile-style relative-ish paths pasted into a field that expects an absolute normalized path; probing/misconfiguration where the credential path was meant to point inside a directory tree.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/1d3cb44a3147b706. Report an issue: GitHub.