denoland/deno · critical · Error

Linter plugin must be an object

Error message

${prefix}Linter plugin must be an object

What it means

A path-traversal (Zip Slip) guard inside the jsr-compat tarball extraction: each entry's path is stripped of its leading package-root component, and the stripped remainder is rejected if it is absolute or contains `..`/root components — i.e. if the entry would resolve outside the destination directory. This is a deliberate security refusal, not a bug to work around; a hit strongly suggests a maliciously or corruptly packed tarball.

Solutions

  1. Do NOT bypass the guard — treat it as a supply-chain red flag
  2. Identify which dependency it came from (the log names the package being installed just above) and inspect it: check its JSR page, source repo, and recent version bumps
  3. Pin the dependency to the last known-good version in deno.json / lockfile until the package is fixed
  4. Report the package to JSR abuse (security@jsr.io) and rotate any secrets if the code was ever executed

Example fix

// deno.json — before (pulling compromised latest)
"imports": { "pkg": "jsr:@evil/pkg" }

// after (pin known-good, audit)
"imports": { "pkg": "jsr:@evil/pkg@0.9.1" }  // last clean version, pending maintainer fix
Defensive patterns

Strategy: validation

Validate before calling

# Audit dependency tarballs for traversal entries before wide rollout
# (the guard itself is the runtime validation — this is defense in depth)
deno info --json 2>/dev/null | jq '.modules[].specifier'  # review your remote deps

Try / catch

Never catch-and-continue this error. In automation, match /Refusing to extract tar entry outside dest/ and escalate immediately: halt the build, notify security, pin the offending dependency to its last known-good version, and audit lockfile diffs.

Prevention

When it happens

Trigger: Extracting a jsr compat tarball (downloaded from npm.jsr.io during `deno check`/install with stock tsc) that contains entries like `pkg/../../evil.sh`, absolute paths (`/etc/...`), or root-component paths. The check operates on the path after skipping the first component, so only genuinely escaping entries are refused.

Common situations: Supply-chain attack via a compromised package; a corrupted download producing garbage paths; a broken pack tool emitting absolute entry names. Genuine packages virtually never trip this guard.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/ff32b942149b9499. Report an issue: GitHub.

Appendix: source

Thrown at cli/js/40_lint.js:483

    }
  }

  return plugins.map((plugin, i) => installPlugin(plugin, specifiers?.[i]));
}

/**
 * @param {Deno.lint.Plugin} plugin
 * @param {string} [specifier] The specifier the plugin was loaded from, if any.
 */
function installPlugin(plugin, specifier) {
  // When the plugin was loaded from a specifier (i.e. `lint.plugins` in the
  // config), prefix validation errors with it so the user knows which plugin
  // is misbehaving instead of getting an anonymous error.
  const prefix = typeof specifier === "string"
    ? `Failed to load lint plugin '${specifier}': `
    : "";
  if (typeof plugin !== "object") {
    throw new Error(`${prefix}Linter plugin must be an object`);
  }
  if (typeof plugin.name !== "string") {
    throw new Error(`${prefix}Linter plugin name must be a string`);
  }
  if (!/^[a-z-]+$/.test(plugin.name)) {
    throw new Error(
      `${prefix}Linter plugin name must only contain lowercase letters (a-z) or hyphens (-).`,
    );
  }
  if (plugin.name.startsWith("-") || plugin.name.endsWith("-")) {
    throw new Error(
      `${prefix}Linter plugin name must start and end with a lowercase letter.`,
    );
  }
  if (plugin.name.includes("--")) {
    throw new Error(
      `${prefix}Linter plugin name must not have consequtive hyphens.`,
    );

View on GitHub (pinned to 9ad36f7a2c)