vercel-labs/skills · critical

Unsafe archive path: ${path}

Error message

Unsafe archive path: ${path}

What it means

Inside addArchiveFile, every archive entry path must pass normalizeArchivePath; if it returns falsy (null/empty) the path is unsafe (traversal, absolute, or malformed) and the provider refuses to store the file. This is the well-known provider's zip-slip guard.

Source

Thrown at src/providers/wellknown.ts:703

    if (rawPath.startsWith('/') || rawPath.startsWith('\\')) return null;
    if (/^[A-Za-z]:/.test(rawPath)) return null;
    if (rawPath.includes('\\')) return null;

    const parts = rawPath.split('/').filter(Boolean);
    if (parts.length === 0) return null;
    if (parts.some((part) => part === '.' || part === '..')) return null;

    return parts.join('/');
  }

  private addArchiveFile(
    files: Map<string, WellKnownFileContent>,
    path: string,
    content: Uint8Array,
    runningTotal: { bytes: number }
  ) {
    const normalizedPath = this.normalizeArchivePath(path);
    if (!normalizedPath) throw new Error(`Unsafe archive path: ${path}`);

    runningTotal.bytes += content.byteLength;
    if (runningTotal.bytes > MAX_ARCHIVE_UNPACKED_BYTES) {
      throw new Error('Archive exceeds maximum unpacked size');
    }
    if (files.size >= MAX_ARCHIVE_FILES) {
      throw new Error('Archive contains too many files');
    }

    files.set(normalizedPath, content);
  }

  private extractTarGz(bytes: Uint8Array): Map<string, WellKnownFileContent> {
    const tar = gunzipSync(Buffer.from(bytes));
    const files = new Map<string, WellKnownFileContent>();
    const runningTotal = { bytes: 0 };
    let offset = 0;

View on GitHub (pinned to 435076e789)

Solutions

  1. Download the artifact manually and list entries (unzip -l / tar -tzf) to find the offending path
  2. Rebuild the artifact with relative POSIX entry names from inside the skill directory
  3. Verify the artifact URL/content checksum; re-publish the registry artifact
  4. Treat repeated occurrences as a security signal and stop trusting that registry source
Defensive patterns

Strategy: validation

Validate before calling

function normalizeArchivePath(p: string): string | null {
  if (!p || p.includes('\0')) return null;
  const n = p.replace(/\\/g, '/').replace(/^\/+/, '');
  if (n.split('/').some((s) => s === '..' || s === '')) return null;
  return n;
}
if (!normalizeArchivePath(entryPath)) skipAndWarn(entryPath);

Type guard

function isUnsafeArchivePath(e: unknown): e is Error {
  return e instanceof Error && /Unsafe archive path/.test(e.message);
}

Try / catch

try { await provider.fetchSkills(); }
catch (e) {
  if (isUnsafeArchivePath(e)) { auditLog.warn(`hostile artifact from registry: ${e.message}`); return []; }
  throw e;
}

Prevention

When it happens

Trigger: A fetched registry archive containing entries like '../x', '/etc/passwd', empty names, or names that normalize to nothing — normalizeArchivePath rejects them and this error throws during extraction.

Common situations: Compromised or mis-built registry artifacts; archives produced by tools emitting absolute paths; corrupted downloads that scramble entry names.

Related errors


AI-assisted analysis of vercel-labs/skills@435076e789 (2026-08-28). Data as JSON: /api/errors/9fe14a40b70fd389. Report an issue: GitHub.