CubeICE の Zip Slip 脆弱性に関する調査報告

先ほど「Zip Slip」と命名された圧縮・解凍処理に関する脆弱性の情報を目にしました。

この記事では、Zip Slip の概要および CubeICE における関連部分の処理内容について記載します。尚、CubeICE に関しては、少なくとも 0.8.0β 以降であれば、Zip Slip 脆弱性の原因とされる不都合はない 事を Snyk によって公開されている サンプルファイル を利用したテスト等によって確認しています。テストコードは ArchiveReaderTest を参照下さい。また、これに合わせて、CubeICE 0.8.0β 未満をご利用のユーザは、可能な限り最新版のご利用をお願いします(参考:圧縮・解凍ソフト CubeICE をゼロから改修)。

Zip Slip 確認用のサンプルファイルを CubeICE で解凍した結果

Zip Slip 脆弱性の概要

Zip Slip は、圧縮ファイルのファイルリスト(ヘッダ部分)に ".." (double-dot) を含むパスが存在する場合、親ディレクトリに存在するファイルがユーザの意図しない形でコピーまたは上書きされる directory traversal と呼ばれる脆弱性の一種のようです。例えば、Zip Slip の解説サイトでは tmp/evil.sh と言うパスの前に大量の ".." を付記しておく事で、/tmp ディレクトリに悪意のあるファイルを作成する手順が示されています。

In the example below, we can see the contents of a zip file. It has two files, a good.sh file which would be extracted into the target directory and an evil.sh file which is trying to traverse up the directory tree to hit the root and then add a file into the tmp directory. When you attempt to cd .. in the root directory, you still find yourself in the root directory, so a malicious path could contain many levels of ../ to stand a better chance of reaching the root directory, before trying to traverse to sensitive files.

5 Tue Jun 5 11:04:29 BST 2018 good.sh
20 Tue Jun 5 11:04:42 BST 2018 ../../../../../../../../tmp/evil.sh

Zip Slip Vulnerability

圧縮ファイルのファイルリストに含まれる ".." を処理する際には注意が必要である、と言う問題自体は古くから認識されています。例えば、先日 iOS や Android 上の多数のアプリに「ZipperDown」と命名された脆弱性が存在すると報道されましたが、これも directory traversal に起因するものだったようです。

しかし、今回の脆弱性に関しても、ファイルリストに含まれるパスをそのまま扱っている事例が思いの外多かったのか Zip Slip の影響を受けるライブラリ もかなりの数に上っています。Snyk では、例えば下記のように、ライブラリから取得出来た名前(相対パス)をそのまま結合するようなコードが含まれている場合、特に注意を要する旨が記載されています。

Enumeration<ZipEntry> entries = zip.getEntries();
while (entries.hasMoreElements()) {
    ZipEntry e = entries.nextElement();
    File f = new File(destinationDir, e.getName());
    InputStream input = zip.getInputStream(e);
    IOUtils.copy(input, write(f));
}

CubeICE における圧縮ファイル中のパスの扱い

CubeICE では、圧縮ファイルのヘッダ情報から取得したパスに対して PathFilter と言う自作クラスを介してアクセスしています。PathFilter は、指定されたパスをディレクトリ単位で分割し、それぞれのファイル名またはディレクトリ名に対して下記のようなチェックを実行します。また、CubeICE では 圧縮・解凍時に特定のファイルまたはディレクトリをフィルタリングする機能を提供していますので、その判定も PathFilter が担っています。

  • カレントディレクトリを表す "." (single-dot) が含まれているかどうか
  • 親ディレクトリを表す ".." (double-dot) が含まれているかどうか
  • ドライブ文字 (drive-letter) が含まれているかどうか
  • UNC パスを表す接頭辞 "\\" が含まれているかどうか
  • サービス機能の不活性化を表す接頭辞 "\\?\" が含まれているかどうか
  • ファイル名またはディレクトリ名の一部に使用不可能な記号が含まれているかどうか
  • ファイル名またはディレクトリ名に Windows で予約済みの名前が存在するかどうか

この PathFilter を実際に利用しているコードが下記 (ArchiveItemController) になります。ただし、これは圧縮ファイルの 1 項目を表す ArchiveItem クラスの内部実装なので、普段はその存在を意識する事はありません。

_filter = new PathFilter(RawName)
{
    AllowParentDirectory  = false,
    AllowDriveLetter      = false,
    AllowCurrentDirectory = false,
    AllowInactivation     = false,
    AllowUnc              = false,
};

src.FullName = _filter.Result;

上記の各種 AllowXxx プロパティでは、対応するシンボル(single-dot, double-dot, drive-letter, ...) の存在を許容するかどうかを設定しています。PathFilter は許容しない場合には該当のシンボルを除去するため、結果として CubeICE ではこれらの特殊なシンボルを全て無視します。例えば、脆弱性の解説において例示されていた "../../../../../../../../tmp/evil.sh" と言うパスは、単に "tmp/evil.sh" として扱います。この処理は、基本となるライブラリ部分で強制的に実行しているため、Readme に記載している下記のサンプルコードにおいても、今回の脆弱性の影響を受ける事はありません。

// Set password directly or using Query<string>
var password = new Cube.Query<string>(e => e.Result = "password");
using (var reader = new ArchiveReader(@"path\to\archive", password))
{
    reader.Extract(@"path\to\directory");
}