denoland/deno · error · AnyError

refusing to write tarball with unsafe name derived from…

Error message

refusing to write tarball with unsafe name derived from package: {}

What it means

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.

Solutions

  1. Use a conventional package name: letters, digits, hyphens, underscores, optionally `@scope/name`
  2. Remove any `..` sequence from the name field and re-run pack
  3. Audit whatever generated the config — names like this indicate a broken generator or an injection attempt

Example fix

// before (deno.json)
{ "name": "../evil-pkg", "version": "1.0.0" }

// after
{ "name": "@scope/evil-pkg", "version": "1.0.0" }
Defensive patterns

Strategy: type-guard

Validate before calling

export function assertSafePackageName(name: string): void {
  const normalized = name.replaceAll('@', '').replaceAll('/', '-');
  if (normalized.includes('..') || normalized.includes('/')) {
    throw new Error(`unsafe package name: ${name}`);
  }
}

Type guard

const isSafePackageName = (name: string): boolean => {
  const normalized = name.replaceAll('@', '').replaceAll('/', '-');
  return name.length > 0 && !normalized.includes('..') && !normalized.includes('/');
};

Prevention

When it happens

Trigger: 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.

Common situations: 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.

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 denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/6d9f53ef2635a447. Report an issue: GitHub.

Appendix: source

Thrown at cli/tools/pack/npm_tarball.rs:37

pub fn default_tarball_filename(
  config_file: &ConfigFile,
  version: &str,
) -> Result<PathBuf, AnyError> {
  let name = config_file
    .json
    .name
    .as_ref()
    .ok_or_else(|| deno_core::anyhow::anyhow!("Missing name"))?;
  // Convert @scope/name to scope-name
  let normalized = name.replace('@', "").replace('/', "-");
  // The package name shape is checked against `@scope/name` higher up
  // (see `pack` in mod.rs), but that check is loose — it does not
  // forbid path-traversal sequences. Treat this as a hard safety
  // boundary right before we open a file, rejecting any derived
  // tarball name that contains `..` or path separators so we never
  // escape the cwd regardless of upstream validation drift.
  if normalized.contains("..") || normalized.contains('/') {
    return Err(deno_core::anyhow::anyhow!(
      "refusing to write tarball with unsafe name derived from package: {}",
      name
    ));
  }
  Ok(PathBuf::from(format!("{}-{}.tgz", normalized, version)))
}

/// Tar archive paths must use forward slashes, even on Windows. Output paths
/// are computed with platform separators when they pass through `Path::display`,
/// so normalize before writing the tar header. The replace is unconditional, so
/// on POSIX — where a backslash is a legal filename character — it can split one
/// legal name into several archive segments. `validate_tar_path` runs on the
/// result to catch any `..` this introduces.
fn to_tar_path(relative: &str) -> String {
  relative.replace('\\', "/")
}

/// Reject archive paths that could escape the extraction root.

View on GitHub (pinned to 9ad36f7a2c)