Zip Slip

未検証のZIPエントリーパスによる抽出先ディレクトリ外への書き込み

説明

ZIPエントリーの名前を検証せずに抽出先のパスとして使うと、../ を含む名前によって、対象ディレクトリの外にファイルを書き込まれるおそれがあります。

想定される影響

  • 任意のファイルへの書き込み、設定の上書き、Webルート内へのファイル作成につながる可能性があります。

対処方法

各エントリーの最終的な正規化パスを計算し、対象ディレクトリの境界から外れる場合は抽出しないでください。攻撃者が書き込めないサービス専用の場所の下に、新しい抽出先ディレクトリを作り、既存のファイルは上書きしません。

例

変更前

csharp
using System.IO;
using System.IO.Compression;

sealed class ZipExtractor
{
    public static void ExtractEntry(
        ZipArchiveEntry entry,
        string targetDirectory)
    {
        var path = Path.GetFullPath(Path.Combine(targetDirectory, entry.FullName));
        entry.ExtractToFile(path, overwrite: true);
    }
}

変更後

csharp
using System;
using System.IO;
using System.IO.Compression;

static string ExtractToNewDirectory(ZipArchive archive, string serviceOwnedRoot)
{
    // ほかのユーザーやプロセスがserviceOwnedRootに書き込めないようにしてください。
    var targetRoot = Directory.CreateDirectory(
        Path.Combine(serviceOwnedRoot, Path.GetRandomFileName())).FullName;
    var targetPrefix = Path.GetFullPath(targetRoot) + Path.DirectorySeparatorChar;

    foreach (var entry in archive.Entries)
    {
        var path = Path.GetFullPath(Path.Combine(targetRoot, entry.FullName));
        if (!path.StartsWith(targetPrefix, StringComparison.Ordinal))
        {
            throw new InvalidDataException("Archive entry escapes the target directory");
        }

        if (string.IsNullOrEmpty(entry.Name))
        {
            Directory.CreateDirectory(path);
            continue;
        }

        Directory.CreateDirectory(Path.GetDirectoryName(path)!);
        entry.ExtractToFile(path, overwrite: false);
    }

    return targetRoot;
}

説明:

  • 変更前: 正規化した抽出先を検証しないため、../ を含むエントリーが対象ディレクトリの外に書き込まれるおそれがあります。
  • 変更後: サービスだけが書き込める場所に、毎回新しい抽出先ディレクトリを作ります。Ordinal による境界の確認は、大文字と小文字を区別するファイルシステムでもパスの逸脱を防ぎます。overwrite: false により、既存のファイルも保持します。

参考資料