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

  1. Reject and avoid the archive; it is unsafe to extract into the assets tree.
  2. Ensure destDir is canonicalized and absolute (it is via Path.GetFullPath) and on a local, non-symlinked volume.
  3. 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

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


AI-assisted analysis of CoplayDev/unity-mcp@c21bf496bc (2026-08-13). Data as JSON: /api/errors/abf80c50f4b08c3b. Report an issue: GitHub.