pnpm/pnpm · error · GitFetcherError

non-normal path component in CAS entry: {rel}

Error message

non-normal path component in CAS entry: {rel}

What it means

The same `join_checked` traversal guard also walks the entry's path components: `Component::Normal` segments are appended, `CurDir` (`.`) is skipped, and `ParentDir` (`..`), `RootDir`, or a Windows `Prefix` component aborts with InvalidInput 'non-normal path component in CAS entry'. This blocks `..`-based escapes out of the CAS root even when the path is technically relative.

Source

Thrown at pnpm/crates/git-fetcher/src/cas_io.rs:60

/// extraction side already get path-traversal guards in
/// `pnpm-tarball`, but defense-in-depth at this layer means a
/// future caller (or a bug in that earlier sanitiser) can't turn
/// a malformed entry into a write outside the working tree.
fn join_checked(root: &Path, rel: &str) -> Result<PathBuf, GitFetcherError> {
    let rel_path = Path::new(rel);
    if rel_path.is_absolute() {
        return Err(GitFetcherError::Io(io::Error::new(
            io::ErrorKind::InvalidInput,
            format!("absolute path is not allowed in CAS entry: {rel}"),
        )));
    }
    let mut out = root.to_path_buf();
    for c in rel_path.components() {
        match c {
            Component::Normal(seg) => out.push(seg),
            Component::CurDir => {}
            Component::ParentDir | Component::RootDir | Component::Prefix(_) => {
                return Err(GitFetcherError::Io(io::Error::new(
                    io::ErrorKind::InvalidInput,
                    format!("non-normal path component in CAS entry: {rel}"),
                )));
            }
        }
    }
    Ok(out)
}

/// Copy every CAS file referenced in `cas_paths` into `target_dir`,
/// preserving relative paths. CAS files are hardlinked-or-copied per
/// install elsewhere, but for the prepare phase the working tree must
/// be writable *without* mutating the shared CAS entry, so this path
/// always allocates fresh inodes via [`fs::copy`].
///
/// Produces a *standalone* directory rather than a CAFS slot —
/// pacquet's `StoreDir` only knows how to import on the way *in*, and
/// the prepare phase needs raw filesystem semantics for scripts to

View on GitHub (pinned to 6261b7f388)

Solutions

  1. Note the printed `{rel}` entry and which dependency produced it
  2. Remove that git dependency's entry from the store and re-fetch from a trusted ref
  3. Report the package and path to pnpm maintainers — normal data never reaches this guard
  4. Treat repeated occurrences as a possible supply-chain attack and audit the dependency source
Defensive patterns

Strategy: validation

Validate before calling

fn cas_entry_has_no_escape(rel: &str) -> bool {
    use std::path::{Component, Path};
    Path::new(rel)
        .components()
        .all(|c| !matches!(c, Component::ParentDir | Component::RootDir | Component::Prefix(_)))
}

Type guard

fn is_safe_cas_rel(rel: &str) -> bool {
    use std::path::{Component, Path};
    let p = Path::new(rel);
    !p.is_absolute()
        && p.components().all(|c| matches!(c, Component::Normal(_) | Component::CurDir))
}

Try / catch

match import_into_cas(&root, &cas_paths) {
    Err(GitFetcherError::Io(ref e))
        if e.kind() == std::io::ErrorKind::InvalidInput
            && e.to_string().contains("non-normal path component") => {
        // a `..`/root/prefix component survived upstream sanitising: reject the entry
    }
    other => other.map_err(Into::into),
}

Prevention

When it happens

Trigger: A `cas_paths` entry containing `..` segments (or a root/prefix component) reaching the git-fetcher's materialize/import step — e.g. `../../etc/passwd` surviving from a crafted git tree or a tarball-sanitizer bug.

Common situations: Malicious git dependencies or tarballs with traversal paths; corrupted store index data; upstream sanitiser regressions.

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


AI-assisted analysis of pnpm/pnpm@6261b7f388 (2026-08-17). Data as JSON: /api/errors/89b94faae29aadba. Report an issue: GitHub.