Prevent Zip Slip in .NET

  • Gérald Barré

This post is part of the series 'Vulnerabilities'. Be sure to check out the rest of the blog posts of the series!

In the previous post, I wrote about zip bombs. It was about not extracting files that are too big. One of the points of the previous post was about not trusting the zip entry headers. The vulnerability I'll explain in this post is quite the same. This time it's about the path traversal.

A zip file contains a flat list of entries. Each entry contains the path of the file (e.g. folder/subfolder/sample.txt), the size of the file, a checksum, and a few other things. The problem is that the path can be whatever you want. For example, it can be:

- a.txt
- folder/b.txt
- folder/c.txt
- folder/subfolder/c.txt
- ../attack.aspx         👈 This can be dangerous...

If you extract this archive, the file attack.aspx will be outside of the folder where you extract the archive. In the case of a website, it means you can replace an existing file, or add a new one. The attacker will get the same privileges as your website on the server. This means it will be possible to execute any requests to your databases, to get the secrets from your configuration files, or any other bad things 😈

Here's the code to create a malicious zip archive:

using (var fs = File.OpenWrite("test.zip"))
using (var archive = new ZipArchive(fs, ZipArchiveMode.Create))
{
    var entry = archive.CreateEntry("../test.txt", CompressionLevel.NoCompression);
    using (var entryStream = entry.Open())
    using (var streamWriter = new StreamWriter(entryStream))
    {
        streamWriter.Write("test");
    }
}

While extracting the archive, you will concatenate the destination path and the path of the entry with a code similar to Path.Combine(destinationDirectoryFullPath, entry.FullName). Then, you must check the path is under the destination directory.

static void ExtractRelativeToDirectory(ZipArchive archive, string destinationDirectoryName, bool overwrite)
{
    foreach (var entry in archive.Entries)
    {
        ExtractRelativeToDirectory(entry, destinationDirectoryName, overwrite);
    }
}

static void ExtractRelativeToDirectory(ZipArchiveEntry entry, string destinationDirectoryName, bool overwrite)
{
    if (entry == null)
        throw new ArgumentNullException(nameof(entry));

    if (destinationDirectoryName == null)
        throw new ArgumentNullException(nameof(destinationDirectoryName));

    // Note that this will give us a good DirectoryInfo even if destinationDirectoryName exists:
    DirectoryInfo di = Directory.CreateDirectory(destinationDirectoryName);
    string destinationDirectoryFullPath = di.FullName;
    if (!destinationDirectoryFullPath.EndsWith(Path.DirectorySeparatorChar))
    {
        destinationDirectoryFullPath += Path.DirectorySeparatorChar;
    }

    string fileDestinationPath = Path.GetFullPath(Path.Combine(destinationDirectoryFullPath, entry.FullName));

    // Ensure we are not extracting a file outside of the destinationDirectoryName
    if (!fileDestinationPath.StartsWith(destinationDirectoryFullPath, StringComparison.OrdinalIgnoreCase))
        throw new Exception($"entry '{entry.FullName}' is outside of the destination directory");

    if (Path.GetFileName(fileDestinationPath).Length == 0)
    {
        // If it is a directory:
        if (entry.Length != 0)
            throw new Exception($"The entry '{entry.FullName}' is a directory but contains data");

        Directory.CreateDirectory(fileDestinationPath);
    }
    else
    {
        // If it is a file:
        // Create containing directory:
        Directory.CreateDirectory(Path.GetDirectoryName(fileDestinationPath));
        entry.ExtractToFile(fileDestinationPath, overwrite: overwrite);
    }
}

This code comes from the implementation of .NET (source), so if you use the method ZipFile.ExtractToDirectory or ZipArchive.ExtractToDirectory you are already protected. If you extract the files manually, you should be very careful.

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee