ZIP解凍を行うには(標準ライブラリ編)

◆PROCESSING 逆引きリファレンス

 カテゴリー:ファイル操作

ZIP解凍を行うには(標準ライブラリ編)

【概要】

PROCESSINGはJavaをベースにした言語ですので、Javaの機能を利用してZIPファイルの解凍を行うことが可能です。

ただしJavaの標準機能(標準ライブラリ)では、パスワード付きのZIPファイルを解凍することができません。

パスワード付きのZIPファイルを解凍したい場合は、標準ライブラリではなく、zip4j ライブラリを利用するのが良いでしょう。

zip4jを用いた解凍方法については「ZIP解凍を行うには(zip4j編)」記事を参照してください。

またZIPファイルの解凍に際してはセキュリティを考慮する必要があります。安易に解凍処理を作成すると、思わゆセキュリティ事故につながる事があるので注意してください

詳しくは下記サイト様の記事を参照してください。本記事でも下記サイト様の記事を元に説明を行います。

 

【詳細】

ZIP解凍を行うには

  • 圧縮されたファイルを元に、ZipInputStreamを作成
  • ZipInputStreamから、ZipEntryを取得
  • チェックしながら解凍
  • ZipEntryとZipInputStreamをcloseする

という手順で処理を行います。

以下、順番に説明をします。

 

ZipInputStreamを作成

ZipInputStreamは、ZIPファイルの解凍を行う基本となるクラスです。圧縮ファイル名を元にFileInputStreamを作成し、それを与えることでインスタンスを作成します。

ZipInputStreamを作成ZipInputStream zis = new ZipInputStream(InputStream is);
ZipInputStream zis = new ZipInputStream(InputStream is, Charset charset);

zis:作成したZipInputStreamインスタンス
is:入力対象となるファイルストリーム
charset:エントリ名とコマンドに使用する文字コード

ポイントは文字コードの指定です。

圧縮ファイルが(日本版)Windowsで作られたものなら、文字コードに「Shift_JIS」または「MS932(windows-31j)」が使われている確率が高いです。

なぜなら(日本版)Windowsでは、ZIPファイルに含まれるファイル名やフォルダ名はMS932(windows-31j)である事がデフォルトとなっているからです。

その一方で圧縮ファイルがMacやLinuxで作られたものなら、文字コードに「UTF-8」が使われている可能性があります。

例えば上記のようになります。Charsetで指定可能な文字コードについては、公式ドキュメント を参照してください。

 

ZipEntryを取得

作成したZipInputStreamから、getNextEntry()メソッドで解凍対象となるファイルのエントリー情報を取得します。

エントリーはZipEntryクラスのインスタンスとして取得されます。

ZipEntryを取得ZipEntry entry = zis.getNextEntry();

zis:ZipInputStreamインスタンス
entry:エントリー情報

取得できない場合はIOExceptionかZipExceptionの例外が発生します。エントリーが無い場合はnullが戻されます。

エントリーが取得出来たら、圧縮対象ファイルの中身を読み出します。すべて読みだしたらZipInputStreamのcloseEntry()メソッドでエントリーを閉じます。

一連の流れは以下のようになります。

 

チェックしながら解凍

特に難しいことを考えないのであれば、Entryを開いて、ファイル書き込み用のOutputStreamへZipInputStreamのread()メソッドで得られたデータを順次書き込めば良いことになります。

次のような感じです(注:以下のロジックは流用しないで下さい。脆弱性があるためです

一見問題ないように見えますが、上記のコードには以下のような問題があります。

  • entry.getName()の正当性を検査していない
  • ファイルサイズの正当性を検査していない

 

entry.getName()の正当性を検査する

entry.getName()が解凍側の予期しない場所を指していた場合、思わぬ場所にファイルが作成されてしまう可能性があります。

なぜならZIPファイルのエントリーには、ディレクトリパスを含めることができるからです。これを悪用すると、システムの重要なファイルを勝手に書き換えられる攻撃(ディレクトリトラバーサル攻撃)が成立します。

以下はわかりやすいように、試験的に作成したディレクトリパス付きのZIPファイルです。


このようなZIPファイルを上記ロジックで解凍すると、例えば「c:\\temp」の下で解凍作業を行ったはずなのに、「c:\\重要書類」というフォルダにある大切なファイル(重要なファイルだよ.txt)が上書き更新されてしまいます(当たり前ですが、決して悪用してはいけません。念のため)。

このような攻撃を防ぐには、今から解凍しようとしているEntry情報のパスが、解凍先のカレントフォルダー配下になっているかどうかを検査します。

あるいはEntry情報のパスがどこであっても、強制的に解凍先のフォルダー配下とみなして解凍するという方法もあります。

今から解凍しようとしているEntry情報のパスが、解凍先のカレントフォルダー配下になっているかどうかを検査するには、下記のようにします(JPCERT コーディネーションセンター 様に掲載されていたものを改編)。

これを

のように呼び出します。

こうする事でディレクトリパス付きのZIPファイルが、意図しない場所に解凍される事を防止できます。

 

ファイルサイズの正当性を検査する

もう1つの問題は、対象ZIPファイルにあるファイルのサイズについてです。異常に大きなファイルが圧縮されていると、解凍した際にDISKやメモリ容量を大量に消費してしまいます。もちろんCPUも大忙しになりますね(汗)。

またZIPファイルでは、ZIPファイルの中に複数のZIPファイルを入れ子状態で格納する事が可能です。


例えば上記では、「怪しい.zip」ファイルの中に2つのZIPファイルが含まれています。

これ自体は問題ないのですが、これを悪用するとZIP爆弾と呼ばれる攻撃が成立してしまいます。具体的には大量のZIPファイルを入れ子状態に圧縮する事で、解凍するコンピュータ側を動作不能に陥らせる攻撃です(こちらも、決して悪用してはいけません)。

このような攻撃を防止するためには、ZIP解凍するファイルサイズを実際に解凍しながら検査し、一定以上のサイズになったら解凍を中止する事です。

ZIPファイルの展開後のサイズは、ZipEntryのgetSize()メソッドで得ることが可能です。が・・・このサイズは信用してはいけません(泣)。

もちろん正常なZIPファイルであれば信用してよいのですが、悪意のあるZIPファイルの場合は、展開後のサイズを詐称する事が可能だからです。ですので処理時間はかかってしまいますが、少しずつ解凍しながら検査を行います。

例えば上記のような感じです。

この例であれば、解凍した複数ファイルの合計が10Kバイトを超えたら、そこで解凍処理が中断されます。

実際のプログラムでは解凍先のディレクトリが存在しない場合も考慮しつつ、処理を記述する必要があります。

 

【関連記事】

 


サンプルプログラム

ZIPファイルを解凍する例:

上記のサイズチェックと解凍先パスの検査処理は、JPCERT コーディネーションセンター 様に掲載されていたものを改編しています。

本例では、targetFile(”c:\\temp\\hoge.zip”)を destDir(”c:\\temp”)配下に解凍します。解凍するファイルサイズがTOTAL_SIZE(上記では10240=10K)を超えると、解凍できないようにしています。

<出力サンプル>

解凍した様子です。下図のような位置関係にあります。

 


PROCESSING逆引きリファレンス一覧 へ戻る

本ページで利用しているアイコン画像は、下記サイト様より拝借しております。各画像の著作権は、それぞれのサイト様および作者にあります。