CoplayDev/unity-mcp · critical · IOException
Unsafe zip entry escapes destination: {name}
Error message
Unsafe zip entry escapes destination: {name} What it means
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.
Source
Thrown at MCPForUnity/Editor/Services/AssetGen/Import/SafeZipExtractor.cs:46
string prefix = destFull.EndsWith(Path.DirectorySeparatorChar.ToString())
? destFull
: destFull + Path.DirectorySeparatorChar;
using (FileStream fs = File.OpenRead(zipPath))
using (var archive = new ZipArchive(fs, ZipArchiveMode.Read))
{
foreach (ZipArchiveEntry entry in archive.Entries)
{
string name = entry.FullName;
if (string.IsNullOrEmpty(name)) continue;
// Reject traversal / absolute paths up front.
if (name.Contains("..") || Path.IsPathRooted(name))
throw new IOException($"Unsafe zip entry rejected: {name}");
string target = Path.GetFullPath(Path.Combine(destDir, name));
if (!target.StartsWith(prefix, StringComparison.Ordinal))
throw new IOException($"Unsafe zip entry escapes destination: {name}");
// A directory entry has an empty Name (FullName ends with a separator).
if (string.IsNullOrEmpty(entry.Name))
{
Directory.CreateDirectory(target);
continue;
}
// Allowlist gate: skip anything that isn't an inert asset type the caller permits.
if (allowedExtensions != null && allowedExtensions.Count > 0
&& !allowedExtensions.Contains(Path.GetExtension(entry.Name).ToLowerInvariant()))
{
continue;
}
string parent = Path.GetDirectoryName(target);
if (!string.IsNullOrEmpty(parent)) Directory.CreateDirectory(parent);
View on GitHub (pinned to c21bf496bc)
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.
Defensive patterns
Strategy: try-catch
Validate before calling
// Ensure destDir is canonical and absolute; the extractor already calls GetFullPath.
string destFull = Path.GetFullPath(destDir);
if (!destFull.EndsWith(Path.DirectorySeparatorChar.ToString()))
destFull += Path.DirectorySeparatorChar;
SafeZipExtractor.ExtractTo(zipPath, destFull, allowed); Try / catch
try { SafeZipExtractor.ExtractTo(zipPath, destDir, allowed); }
catch (IOException ex) when (ex.Message.Contains("escapes destination"))
{
// The resolved entry left the destination; refuse the archive outright.
Log.Error($"Refusing archive with escaping entry: {ex.Message}");
throw;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Unsafe zip entry rejected: {name}
- provider returned a disallowed file type '.{ext}'
- zipPath required
- Screenshot folder '{folderOverride}' resolves outside the Un
- CredWrite failed (Win32 {GetLastWin32Error})
AI-assisted analysis of CoplayDev/unity-mcp@c21bf496bc (2026-08-13).
Data as JSON: /api/errors/abf80c50f4b08c3b.
Report an issue: GitHub.