{"record":{"id":"9fe14a40b70fd389","repo":"vercel-labs/skills","slug":"unsafe-archive-path-path","errorCode":null,"errorMessage":"Unsafe archive path: ${path}","messagePattern":"Unsafe archive path: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"src/providers/wellknown.ts","lineNumber":703,"sourceCode":"    if (rawPath.startsWith('/') || rawPath.startsWith('\\\\')) return null;\n    if (/^[A-Za-z]:/.test(rawPath)) return null;\n    if (rawPath.includes('\\\\')) return null;\n\n    const parts = rawPath.split('/').filter(Boolean);\n    if (parts.length === 0) return null;\n    if (parts.some((part) => part === '.' || part === '..')) return null;\n\n    return parts.join('/');\n  }\n\n  private addArchiveFile(\n    files: Map<string, WellKnownFileContent>,\n    path: string,\n    content: Uint8Array,\n    runningTotal: { bytes: number }\n  ) {\n    const normalizedPath = this.normalizeArchivePath(path);\n    if (!normalizedPath) throw new Error(`Unsafe archive path: ${path}`);\n\n    runningTotal.bytes += content.byteLength;\n    if (runningTotal.bytes > MAX_ARCHIVE_UNPACKED_BYTES) {\n      throw new Error('Archive exceeds maximum unpacked size');\n    }\n    if (files.size >= MAX_ARCHIVE_FILES) {\n      throw new Error('Archive contains too many files');\n    }\n\n    files.set(normalizedPath, content);\n  }\n\n  private extractTarGz(bytes: Uint8Array): Map<string, WellKnownFileContent> {\n    const tar = gunzipSync(Buffer.from(bytes));\n    const files = new Map<string, WellKnownFileContent>();\n    const runningTotal = { bytes: 0 };\n    let offset = 0;\n","sourceCodeStart":685,"sourceCodeEnd":721,"githubUrl":"https://github.com/vercel-labs/skills/blob/435076e78988e1e6ec40d00b0b1d76bdbbc5419a/src/providers/wellknown.ts#L685-L721","documentation":"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.","triggerScenarios":"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.","commonSituations":"Compromised or mis-built registry artifacts; archives produced by tools emitting absolute paths; corrupted downloads that scramble entry names.","solutions":["Download the artifact manually and list entries (unzip -l / tar -tzf) to find the offending path","Rebuild the artifact with relative POSIX entry names from inside the skill directory","Verify the artifact URL/content checksum; re-publish the registry artifact","Treat repeated occurrences as a security signal and stop trusting that registry source"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"function normalizeArchivePath(p: string): string | null {\n  if (!p || p.includes('\\0')) return null;\n  const n = p.replace(/\\\\/g, '/').replace(/^\\/+/, '');\n  if (n.split('/').some((s) => s === '..' || s === '')) return null;\n  return n;\n}\nif (!normalizeArchivePath(entryPath)) skipAndWarn(entryPath);","typeGuard":"function isUnsafeArchivePath(e: unknown): e is Error {\n  return e instanceof Error && /Unsafe archive path/.test(e.message);\n}","tryCatchPattern":"try { await provider.fetchSkills(); }\ncatch (e) {\n  if (isUnsafeArchivePath(e)) { auditLog.warn(`hostile artifact from registry: ${e.message}`); return []; }\n  throw e;\n}","preventionTips":["Only consume well-known registries you trust","Pre-validate every entry name before storing extraction results","Alert on unsafe-path failures — they indicate hostile or broken artifacts"],"tags":["security","path-traversal","zip-slip","archive","wellknown-provider"],"backgroundTag":"zip-slip-path-traversal","analyzedSha":"435076e78988e1e6ec40d00b0b1d76bdbbc5419a","analyzedAt":"2026-08-28T17:47:53.369Z","schemaVersion":2},"datasetVersion":"2026-08-28T21:17:43.275Z"}