{"record":{"id":"6d9f53ef2635a447","repo":"denoland/deno","slug":"refusing-to-write-tarball-with-unsafe-name-derived","errorCode":null,"errorMessage":"refusing to write tarball with unsafe name derived from package: {}","messagePattern":"refusing to write tarball with unsafe name derived from package: (.+?)","errorType":"exception","errorClass":"AnyError","httpStatus":null,"severity":"error","filePath":"cli/tools/pack/npm_tarball.rs","lineNumber":37,"sourceCode":"pub fn default_tarball_filename(\n  config_file: &ConfigFile,\n  version: &str,\n) -> Result<PathBuf, AnyError> {\n  let name = config_file\n    .json\n    .name\n    .as_ref()\n    .ok_or_else(|| deno_core::anyhow::anyhow!(\"Missing name\"))?;\n  // Convert @scope/name to scope-name\n  let normalized = name.replace('@', \"\").replace('/', \"-\");\n  // The package name shape is checked against `@scope/name` higher up\n  // (see `pack` in mod.rs), but that check is loose — it does not\n  // forbid path-traversal sequences. Treat this as a hard safety\n  // boundary right before we open a file, rejecting any derived\n  // tarball name that contains `..` or path separators so we never\n  // escape the cwd regardless of upstream validation drift.\n  if normalized.contains(\"..\") || normalized.contains('/') {\n    return Err(deno_core::anyhow::anyhow!(\n      \"refusing to write tarball with unsafe name derived from package: {}\",\n      name\n    ));\n  }\n  Ok(PathBuf::from(format!(\"{}-{}.tgz\", normalized, version)))\n}\n\n/// Tar archive paths must use forward slashes, even on Windows. Output paths\n/// are computed with platform separators when they pass through `Path::display`,\n/// so normalize before writing the tar header. The replace is unconditional, so\n/// on POSIX — where a backslash is a legal filename character — it can split one\n/// legal name into several archive segments. `validate_tar_path` runs on the\n/// 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.","sourceCodeStart":19,"sourceCodeEnd":55,"githubUrl":"https://github.com/denoland/deno/blob/9ad36f7a2cce60488e6ec52283efb32efddaf93a/cli/tools/pack/npm_tarball.rs#L19-L55","documentation":"A deliberate security boundary in default_tarball_filename(): after normalizing the package name (stripping `@` and replacing `/` with `-`), the derived tarball path is rejected if it still contains `..` or `/`, because such a name could make `name-version.tgz` resolve outside the working directory via path traversal. The code comments note the upstream name-shape check is loose, so this guard runs as a hard boundary right before the tarball file is opened for writing.","triggerScenarios":"deno.json `name` containing dot-dot sequences that survive normalization — e.g. `\"..\"`, `\"../evil\"` (normalizes to `..-evil`, which still contains `..`), or any name whose transformed form retains `..` — tripping the check at cli/tools/pack/npm_tarball.rs:37.","commonSituations":"Hand-edited or machine-generated configs with malformed or malicious names; fuzz/security testing of pack; copy-pasting a path fragment into the name field. Rare in normal use — usually broken tooling or tampering.","solutions":["Use a conventional package name: letters, digits, hyphens, underscores, optionally `@scope/name`","Remove any `..` sequence from the name field and re-run pack","Audit whatever generated the config — names like this indicate a broken generator or an injection attempt"],"exampleFix":"// before (deno.json)\n{ \"name\": \"../evil-pkg\", \"version\": \"1.0.0\" }\n\n// after\n{ \"name\": \"@scope/evil-pkg\", \"version\": \"1.0.0\" }","handlingStrategy":"type-guard","validationCode":"export function assertSafePackageName(name: string): void {\n  const normalized = name.replaceAll('@', '').replaceAll('/', '-');\n  if (normalized.includes('..') || normalized.includes('/')) {\n    throw new Error(`unsafe package name: ${name}`);\n  }\n}","typeGuard":"const isSafePackageName = (name: string): boolean => {\n  const normalized = name.replaceAll('@', '').replaceAll('/', '-');\n  return name.length > 0 && !normalized.includes('..') && !normalized.includes('/');\n};","tryCatchPattern":null,"preventionTips":["Validate package names against npm/JSR naming rules when configs are generated","Never build file names from unvalidated config strings","Treat this error as a security signal — investigate the config's origin"],"tags":["security","path-traversal","pack","package-name","validation"],"backgroundTag":"path-traversal-blocked","analyzedSha":"9ad36f7a2cce60488e6ec52283efb32efddaf93a","analyzedAt":"2026-08-20T13:07:44.778Z","contentChangedAt":"2026-08-20T13:07:44.778Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}