圧縮ファイルの圧縮解除処理でパストラバーサルの脆弱性を招く問題とその対応について説明します。
問題
圧縮ファイルの圧縮解除処理で、パストラバーサルの脆弱性を含むコードは以下になります。
| 1 2 3 4 5 6 7 8 9 10 11 12 |        String compressPath = "<圧縮ファイルのパス>"        String unzipDirPath = "<圧縮解除先のディレクトリのパス>"         ZipInputStream zis = null;         try {             FileInputStream fis = new FileInputStream(fileName);             BufferedInputStream bis = new BufferedInputStream(fis);             ZipEntry ze = null;             zis = new ZipInputStream(bis);             while ((ze = zis.getNextEntry()) != null) {                 File file = new File(unzipDirPath, ze.getName());     ... | 
上のコードはZIPファイルを読み込み、指定先に圧縮解除する処理の一部です。脆弱性のある箇所は圧縮されているファイルのファイル名と圧縮解除先のディレクトリのパスから、ファイルを作成している以下のコードです。
| 1 | File file = new File(unzipDirPath, ze.getName()); | 
上のコードですとファイル名に「../」の文字を含んでいる可能性があります。その場合は作成されるファイルのパスは、圧縮解除先のディレクトリより上の階層になります。
例として、
| 1 2 | 圧縮解除先のディレクトリのパス:/hoge/hoge/ ファイル名:../example.txt | 
ですと作成されるファイルのパスは
| 1 | /hoge/example.txt | 
となり、圧縮解除先のディレクトリより上の階層になります。
対応
コードの修正はGoogleヘルプの「Fixing a Zip Path Traversal Vulnerability」のページを参考に行いました。修正としては、作成したパスを正規化し、正規化したパスに圧縮解除先のディレクトリのパスが含まれているかを確認します。修正したコードは以下になります。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |        String compressPath = "<圧縮ファイルのパス>"        String unzipDirPath = "<圧縮解除先のディレクトリのパス>"         ZipInputStream zis = null;         try {             FileInputStream fis = new FileInputStream(compressPath);             BufferedInputStream bis = new BufferedInputStream(fis);             zis = new ZipInputStream(bis);             ZipEntry ze = null;             while ((ze = zis.getNextEntry()) != null) {                 File file = new File(unzipDirPath, ze.getName()).getCanonicalFile();                 // 配置予定のディレクトリより上の階層に作成されないか確認                 if (!file.getPath().startsWith(unzipDirPath)) {                     throw new IOException("this file path is invalid.");                 }     ... | 
修正したコードでは脆弱性を含んでいた箇所を以下のように修正し、パスを正規化しています。
| 1 | File file = new File(unzipDirPath, ze.getName()).getCanonicalFile(); | 
getCanonicalFileメソッドで正規化したパスを持つFileインスタンスを取得します。その直後に以下の処理を追加し、正規化したパスに圧縮解除先のディレクトリのパスが含まれているかを確認します。
| 1 2 3 | if (!file.getPath().startsWith(unzipDirPath)) {     throw new IOException("this file path is invalid."); } | 
startsWithメソッドを使用し、正規化したパスの先頭が圧縮解除先のディレクトリのパスと一致していことを確認しています。正規化したパスに、ディレクトリのパスが含まれている場合は残りの処理を実行します。パスが含まれていない場合はエラーを返します。
この修正により、以下のパスは圧縮解除でき、
| 1 2 3 | 圧縮解除先のディレクトリのパス:/hoge/hoge/ ファイル名:example.txt 作成ファイルのパス:/hoge/hoge/example.txt ファイル名:huga/example.txt 作成ファイルのパス:/hoge/hoge/huga/example.txt | 
以下のようなパスはエラーになります。
| 1 2 3 | 圧縮解除先のディレクトリのパス:/hoge/hoge/ ファイル名:../example.txt 作成ファイルのパス:/hoge/example.txt ファイル名:../../huga/huga/example.txt 作成ファイルのパス:/huga/huga/example.txt | 
この対応ではパスを正規化してパスの確認をしていますので、圧縮解除先のディレクトリとその下の階層のみに制限できます。ただ圧縮ファイル内のファイル名が絶対パスについての考慮は、別の機会に試したいと思います。