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 toView on GitHub (pinned to 6261b7f388)
Solutions
- Note the printed `{rel}` entry and which dependency produced it
- Remove that git dependency's entry from the store and re-fetch from a trusted ref
- Report the package and path to pnpm maintainers — normal data never reaches this guard
- 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
- Reject `..` components where entry paths are produced (tarball extraction, packlist over git trees)
- Treat any hit of this guard as a security signal: audit the source dependency
- Keep sanitizers and this defense-in-depth layer both active — do not rely on one
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
- absolute path is not allowed in CAS entry: {rel}
- CAS path {cas_path:?} for {rel:?} does not match `files/XX/<
- Failed to remove Windows bin shims for ${cmd}
- NODE_SHASUMS_SIGNATURE_INVALID
- NODE_SHASUMS_FETCH_FAIL
AI-assisted analysis of pnpm/pnpm@6261b7f388 (2026-08-17).
Data as JSON: /api/errors/89b94faae29aadba.
Report an issue: GitHub.