{"record":{"id":"abf80c50f4b08c3b","repo":"CoplayDev/unity-mcp","slug":"unsafe-zip-entry-escapes-destination-name","errorCode":null,"errorMessage":"Unsafe zip entry escapes destination: {name}","messagePattern":"Unsafe zip entry escapes destination: (.+?)","errorType":"exception","errorClass":"IOException","httpStatus":null,"severity":"critical","filePath":"MCPForUnity/Editor/Services/AssetGen/Import/SafeZipExtractor.cs","lineNumber":46,"sourceCode":"            string prefix = destFull.EndsWith(Path.DirectorySeparatorChar.ToString())\n                ? destFull\n                : destFull + Path.DirectorySeparatorChar;\n\n            using (FileStream fs = File.OpenRead(zipPath))\n            using (var archive = new ZipArchive(fs, ZipArchiveMode.Read))\n            {\n                foreach (ZipArchiveEntry entry in archive.Entries)\n                {\n                    string name = entry.FullName;\n                    if (string.IsNullOrEmpty(name)) continue;\n\n                    // Reject traversal / absolute paths up front.\n                    if (name.Contains(\"..\") || Path.IsPathRooted(name))\n                        throw new IOException($\"Unsafe zip entry rejected: {name}\");\n\n                    string target = Path.GetFullPath(Path.Combine(destDir, name));\n                    if (!target.StartsWith(prefix, StringComparison.Ordinal))\n                        throw new IOException($\"Unsafe zip entry escapes destination: {name}\");\n\n                    // A directory entry has an empty Name (FullName ends with a separator).\n                    if (string.IsNullOrEmpty(entry.Name))\n                    {\n                        Directory.CreateDirectory(target);\n                        continue;\n                    }\n\n                    // Allowlist gate: skip anything that isn't an inert asset type the caller permits.\n                    if (allowedExtensions != null && allowedExtensions.Count > 0\n                        && !allowedExtensions.Contains(Path.GetExtension(entry.Name).ToLowerInvariant()))\n                    {\n                        continue;\n                    }\n\n                    string parent = Path.GetDirectoryName(target);\n                    if (!string.IsNullOrEmpty(parent)) Directory.CreateDirectory(parent);\n","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/CoplayDev/unity-mcp/blob/c21bf496bca87d54e75bad048563c3adb1782081/MCPForUnity/Editor/Services/AssetGen/Import/SafeZipExtractor.cs#L28-L64","documentation":"After resolving an entry's target to a full path, SafeZipExtractor verifies the resolved path starts with the destination directory prefix (ordinal comparison). If the resolved path escapes the destination despite passing the '..' check (e.g. via normalization or separator tricks), it throws IOException. This is defense-in-depth against zip-slip.","triggerScenarios":"An entry path that normalizes outside destDir; mixed OS path separators that bypass the prefix match; a symlink/junction inside the destination redirecting outside; case/normalization edge cases.","commonSituations":"A cross-platform archive with mixed separators on Windows; a deliberately malicious archive; an edge in path canonicalization between the prefix and the resolved target.","solutions":["Reject and avoid the archive; it is unsafe to extract into the assets tree.","Ensure destDir is canonicalized and absolute (it is via Path.GetFullPath) and on a local, non-symlinked volume.","Report the offending archive to the marketplace provider."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Ensure destDir is canonical and absolute; the extractor already calls GetFullPath.\nstring destFull = Path.GetFullPath(destDir);\nif (!destFull.EndsWith(Path.DirectorySeparatorChar.ToString()))\n    destFull += Path.DirectorySeparatorChar;\nSafeZipExtractor.ExtractTo(zipPath, destFull, allowed);","typeGuard":null,"tryCatchPattern":"try { SafeZipExtractor.ExtractTo(zipPath, destDir, allowed); }\ncatch (IOException ex) when (ex.Message.Contains(\"escapes destination\"))\n{\n    // The resolved entry left the destination; refuse the archive outright.\n    Log.Error($\"Refusing archive with escaping entry: {ex.Message}\");\n    throw;\n}","preventionTips":["Extract onto a local, non-symlinked volume to avoid redirection tricks.","Treat any escaping-entry failure as a security boundary, not a recoverable error.","Canonicalize destDir to an absolute path before extraction."],"tags":["assetgen","zip","security","path-traversal"],"backgroundTag":null,"analyzedSha":"c21bf496bca87d54e75bad048563c3adb1782081","analyzedAt":"2026-08-13T17:36:56.095Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}