本記事はユーザーからの寄稿です。
著者:江湖人士
原文タイトル:2022年末 C# でzipファイルを解凍する際に遭遇したバグ
原文リンク:https://jhrs.com/2022/46060.html
最近、アップロード機能を調査していたところ、クライアントがzipファイルをアップロードし、サーバー側でC#を使用してzipファイルを解凍し、アップロードが許可されたファイルタイプかどうかを検証する処理を行っていました。また、ファイル拡張子の不正な変更やファイルヘッダーなども確認する必要がありましたが、zipファイルを解凍する際に問題が発生しました。
C# で zip ファイルを解凍する
まず前提として、IIS上にファイルサービスサイトをデプロイしており、各種ファイルをアップロードできるようになっています。処理の流れとしては、まずサイトのルートディレクトリ内にランダムに作成した一時ディレクトリ(ここでは手抜き方法としてGUIDをディレクトリ名として使用)にアップロードし、ファイル検証を通過した後にコードで正式なアーカイブディレクトリにコピーまたは移動します。C#でのファイルのコピー・移動のコードは、江湖人士サイトの記事を参考にしてください。昨日の業務終了間際に、zipファイルのアップロード時にエラーが発生し、ファイルサービスのルートサイトに大量のGUIDで始まるディレクトリが作成されてしまいました。これは明らかに異常事態であり、コードにバグがあるに違いありません。

バグのある解凍コード
もうすぐ2022年末だというのに、このバグが発生しました。急いで模擬環境を構築して動作確認したところ、以下の元のコードに問題があることが判明しました。元のコードは次のとおりです。
/// <summary>
/// ファイルを解凍する
/// </summary>
/// <param name="saveDir">保存ディレクトリ</param>
/// <param name="stream"></param>
public static void UnZipFiles(string saveDir, Stream stream)
{
using (ZipInputStream s = new ZipInputStream(stream))
{
ZipEntry theEntry;
while ((theEntry = s.GetNextEntry()) != null)
{
string directoryName = $"{saveDir}{Path.GetDirectoryName(theEntry.Name)}\\";
string fileName = Path.GetFileName(theEntry.Name);
Directory.CreateDirectory(directoryName);
using (FileStream streamWriter = File.Create(directoryName + fileName))
{
byte[] data = new byte[2048];
while (true)
{
int size = s.Read(data, 0, data.Length);
if (size > 0)
{
streamWriter.Write(data, 0, size);
}
else
{
break;
}
}
}
}
}
}
ちなみに、これは古いプロジェクトであるため、圧縮・解凍には ICSharpCode.SharpZipLib.Zip コンポーネントを使用しています。
ソースコードを見ると、一目で問題点がわかりました。ロジックが不十分であり、解凍したファイルの保存ディレクトリを直接連結していることが原因です。
このバグを修正するには?
問題がわかれば修正は簡単で、Path.Combine メソッドを呼び出し、解凍時にディレクトリかファイルかを判断すればよいです。最終的な修正後のコードは以下のとおりです。
/// <summary>
/// ファイルを解凍する
/// </summary>
/// <param name="saveDir">保存ディレクトリ</param>
/// <param name="stream"></param>
public static void UnZipFiles(string saveDir, Stream stream)
{
using (ZipInputStream s = new ZipInputStream(stream))
{
ZipEntry theEntry;
string directoryName, file, fileName;
while ((theEntry = s.GetNextEntry()) != null)
{
directoryName = Path.Combine(saveDir, Path.GetDirectoryName(theEntry.Name));
fileName = Path.GetFileName(theEntry.Name);
Directory.CreateDirectory(directoryName);
file = Path.Combine(directoryName, fileName);
if (theEntry.IsFile)
{
using (FileStream streamWriter = File.Create(file))
{
byte[] data = new byte[2048];
while (true)
{
int size = s.Read(data, 0, data.Length);
if (size > 0)
{
streamWriter.Write(data, 0, size);
}
else
{
break;
}
}
}
}
}
}
}