{"record":{"id":"af7644082b0a1cd5","repo":"siyuan-note/siyuan","slug":"invalid-archive-entry-s-unzip","errorCode":null,"errorMessage":"invalid archive entry [%s]","messagePattern":"invalid archive entry \\[(.+?)\\]","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/mcp/tools/unzip.go","lineNumber":123,"sourceCode":"\tdefer reader.Close()\n\n\t// 成员名与其最终输出路径成对保存，两次遍历使用同一份判定输入\n\ttype archiveMember struct {\n\t\tname string\n\t\tpath string\n\t}\n\tmembers := make([]archiveMember, len(reader.File))\n\tfor i, entry := range reader.File {\n\t\tname := entry.Name\n\t\tif !utf8.ValidString(name) {\n\t\t\t// 与 Gulu 一致地解码 GB18030 条目名，避免解码前后的名称不一致导致校验被绕过\n\t\t\tif name, err = simplifiedchinese.GB18030.NewDecoder().String(name); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tname = strings.ReplaceAll(name, \"\\\\\", \"/\")\n\t\tif !filepath.IsLocal(filepath.FromSlash(name)) || entry.Mode()&os.ModeSymlink != 0 {\n\t\t\treturn fmt.Errorf(\"invalid archive entry [%s]\", name)\n\t\t}\n\t\tmembers[i] = archiveMember{name: name, path: filepath.Join(destAbs, filepath.FromSlash(name))}\n\t\tif err = authorizeArchiveEntry(destAbs, members[i].path, members[i].name); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tfor i, entry := range reader.File {\n\t\t// 解压前再次授权，不复用预检阶段的判定结果\n\t\tif err = authorizeArchiveEntry(destAbs, members[i].path, members[i].name); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err = extractArchiveEntry(entry, members[i].path); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/mcp/tools/unzip.go#L105-L141","documentation":"While extracting an archive, extractGuardedArchive validates each member name before writing: filepath.IsLocal must consider the slash-normalized name local (no absolute paths, no \"..\" components, not rooted) and the entry must not be a symlink. Either condition yields this error, blocking classic zip-slip and symlink-based archive attacks.","triggerScenarios":"unzipHandler on an archive containing an entry named like \"/etc/passwd\", \"../escape.txt\", \"a/../../x\", a Windows-style \"..\\evil\" name, or any entry whose mode includes os.ModeSymlink.","commonSituations":"Malicious or hand-crafted zips; archives produced on Windows with backslash traversal names (the code normalizes backslashes first); zips built with tools that store symlinks; legacy archives with rooted entry names.","solutions":["Repack the archive with clean relative entry names (no leading /, no .. segments) and without symlink entries","If the archive legitimately contains symlinks, materialize them as regular files before zipping or extract with a tool outside the MCP guard set (within policy)","Inspect the archive listing (unzip -l) to find the offending entry name and fix or drop it"],"exampleFix":"// before (zip contains entry \"../evil.txt\")\nunzipHandler({\"path\": \"payload.zip\"}) // -> invalid archive entry\n// after (repack with sanitized names)\nzip -r clean.zip evil.txt  # entry name: evil.txt\nunzipHandler({\"path\": \"clean.zip\"})","handlingStrategy":"validation","validationCode":"for (const name of entryNames) {\n  const norm = name.replace(/\\\\/g, '/');\n  if (path.isAbsolute(norm) || norm.split('/').includes('..')) {\n    throw new Error(`unsafe archive entry: ${name}`);\n  }\n}","typeGuard":"function isSafeEntryName(name) {\n  const norm = name.replace(/\\\\/g, '/');\n  return !norm.startsWith('/') && !norm.split('/').includes('..') && norm.length > 0;\n}","tryCatchPattern":"try {\n  await callMcpTool('unzip', { path: zipPath, dest });\n} catch (e) {\n  if (e.message.startsWith('invalid archive entry')) {\n    // reject/quarantine the archive; do not attempt manual extraction\n  }\n}","preventionTips":["Scan archives for .. and absolute entry names before extraction","Repack archives that contain symlink entries as plain files","Only accept archives from trusted sources; treat failures as malicious-input signals"],"tags":["security","archive","zip-slip","path-validation"],"backgroundTag":"path-traversal-blocked","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}