{"record":{"id":"f77c5e2e29be1e79","repo":"denoland/deno","slug":"refusing-to-add-to-tarball","errorCode":null,"errorMessage":"refusing to add {} to tarball: {}","messagePattern":"refusing to add (.+?) to tarball: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cli/tools/pack/npm_tarball.rs","lineNumber":68,"sourceCode":"/// result to catch any `..` this introduces.\nfn to_tar_path(relative: &str) -> String {\n  relative.replace('\\\\', \"/\")\n}\n\n/// Reject archive paths that could escape the extraction root.\n///\n/// `tar::Builder::append_data` only validates the truncated 100-byte header\n/// name when it emits a GNU LongLink entry; the full path is written to the\n/// LongLink data unchecked. So a path >= 100 bytes can smuggle `..` past the\n/// crate's own checks, while a short path is still rejected by `set_path`.\n/// Validate here so both lengths behave the same.\n///\n/// Segments are split off the normalized `&str` rather than via\n/// `std::path::Path::components()`: `Path` parsing is platform-dependent and\n/// would treat these inputs differently on Windows.\nfn validate_tar_path(path: &str) -> std::io::Result<()> {\n  let invalid = |reason: &str| {\n    std::io::Error::new(\n      std::io::ErrorKind::InvalidInput,\n      format!(\"refusing to add {} to tarball: {}\", path, reason),\n    )\n  };\n  let mut segments = path.split('/').peekable();\n  // A leading \"./\" is fine; a leading empty segment means the path is absolute.\n  if segments.peek() == Some(&\".\") {\n    segments.next();\n  }\n  for segment in segments {\n    match segment {\n      \"..\" => return Err(invalid(\"path contains a '..' component\")),\n      \"\" => return Err(invalid(\"path is absolute or empty\")),\n      \".\" => return Err(invalid(\"path contains a '.' component\")),\n      _ => {}\n    }\n  }\n  Ok(())","sourceCodeStart":50,"sourceCodeEnd":86,"githubUrl":"https://github.com/denoland/deno/blob/9ad36f7a2cce60488e6ec52283efb32efddaf93a/cli/tools/pack/npm_tarball.rs#L50-L86","documentation":"`validate_tar_path` runs on every relative path before it is appended to the npm tarball (the `deno publish`/pack path). It rejects paths containing a `..` component, an empty segment (absolute paths), or a stray `.` component. The check exists because `tar::Builder::append_data` only validates the 100-byte header name — long paths go through a GNU LongLink entry whose full path is written unchecked, so explicit validation makes both lengths behave the same and blocks archive path traversal.","triggerScenarios":"A packaged file whose relative path contains `..`, is absolute, or has a `.`/empty segment. Notably, `to_tar_path` unconditionally replaces `\\` with `/` — so a single legal POSIX filename containing backslashes (e.g. `foo\\..\\..\\evil`) is split into traversal-looking segments and rejected. The message names the offending path and the reason.","commonSituations":"Publishing a package containing weird generated files with backslashes in their names; build artifacts written with unvalidated external names; attempts (accidental or malicious) to smuggle `..` entries into a tarball.","solutions":["Locate the printed path on disk (quote it: `ls -la './foo\\..\\..\\evil'`) and delete or rename the offending file, then re-run.","Exclude it from the package via the package.json `files` field or publish excludes so it never reaches tarball assembly.","Fix the generator that produced backslash or `..`-containing filenames so the corpus is clean."],"exampleFix":"// before: a file literally named \"pkg\\..\\..\\evil\" becomes traversal after \\\\ → / normalization\n// after: validate relative paths in your build step, mirroring the pack check\nfn is_tar_safe(rel: &str) -> bool {\n  let mut segments = rel.split('/');\n  if rel.starts_with(\"./\") { segments.next(); }\n  segments.all(|s| !matches!(s, \"\" | \".\" | \"..\"))\n}\nassert!(is_tar_safe(&rel), \"refusing to add {rel} to tarball\");","handlingStrategy":"validation","validationCode":"// run over your packaged file list before publishing\nfn is_tar_safe(rel: &str) -> bool {\n  let mut segments = rel.split('/');\n  if rel.starts_with(\"./\") { segments.next(); }\n  segments.all(|s| !matches!(s, \"\" | \".\" | \"..\"))\n}\nfor rel in &package_files {\n  assert!(is_tar_safe(rel), \"refusing to add {rel} to tarball\");\n}","typeGuard":null,"tryCatchPattern":"match publish() {\n  Err(e) if e.kind() == std::io::ErrorKind::InvalidInput\n      && e.to_string().contains(\"refusing to add\") => {\n    // the message names the exact path — remove/exclude it and re-run\n  }\n  other => other,\n}","preventionTips":["Never generate filenames containing backslashes on POSIX","Validate relative paths of packaged files in the build step","Keep publish excludes (files field) tight so odd artifacts never reach tarball assembly"],"tags":["deno-publish","npm-tarball","path-traversal","security","archive"],"backgroundTag":"path-traversal-in-archive","analyzedSha":"9ad36f7a2cce60488e6ec52283efb32efddaf93a","analyzedAt":"2026-08-20T13:07:44.778Z","schemaVersion":2},"datasetVersion":"2026-08-31T09:17:48.483Z"}