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
- 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
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
- 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
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
- Invalid package name
- package name must not contain dot path segments
- ERR_INVALID_HTTP_TOKEN
- ERR_INVALID_HTTP_TOKEN
- ERR_INVALID_HTTP_TOKEN
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)