Java API [API 2006] の java.io.File クラスの説明には以下のように書かれている。
パス名(抽象形式または文字列形式のどちらでも)は、「絶対」また「相対」のどちらかである。絶対パス名は完全であり、それが示すファイルを見つけるためにほかの情報を必要としない。一方、相対パス名は、他のパス名からの情報を利用して解釈される必要がある。
絶対パス名や相対パス名には、シンボリックリンク(ソフトリンク)、ハードリンク、ショートカット、シャドー、エイリアス、ジャンクションといった、他のパス名へのリンクが含まれている可能性がある。このようなファイルは、ファイルを検証する前に正規化しなければならない。たとえば、「trace」というシンボリックリンクが指すファイルの実体は /home/system/trace であるかもしれない。また、パス名の中には、ファイルの検証を困難にする特殊ファイルが含まれているかもしれない。
- 「.」はカレントディレクトリ自身を表す
- 「..」は親ディレクトリを表す
パス名の検証を難しくする要因としてはこれ以外にも、オペレーティングシステムやファイルシステム固有の命名規則がある。
ファイル名を正規化することでパス名の検証は容易になる。複数の異なるパス名は同一のディレクトリやファイルを指すかもしれないし、パス名のテキスト表現からはそれが実際に指すディレクトリやファイルに関する情報はほとんど得られないかもしれない。したがって、すべてのパス名は検証処理を行う前に解決、つまり正規化しなくてはならない。
パス名の検証は、たとえば、特定のディレクトリにあるファイルへのアクセスを制限したい場合や、ファイル名やパス名に応じてセキュリティ上の意志決定を行いたい場合などに必要になる。このような処理は多くの場合、ディレクトリトラバーサルやパス同一性に関する脆弱性を悪用した攻撃によって回避される可能性がある。ディレクトリトラバーサルの脆弱性は、特定のディレクトリの外への入出力操作を許してしまうものである。パス同一性の脆弱性は、攻撃者があるリソースに対して(同じファイルに解決される)別のパス名を指定することでセキュリティチェックを回避できる場合に発生する。
パスの正規化プロセスには、本質的に避けられない競合ウィンドウが存在する。この競合ウィンドウは、正規化パスが取得されてから、ファイルが開かれるまでの間続く。正規化パス名を検証している間にファイルシステムが変更され、正規化パス名が元の正しいファイルを参照しなくなる可能性がある。この競合状態を解決するのは簡単である。正規化パス名がセキュアなディレクトリのなかのファイルを指しているかどうかを確認するのである(詳しくは「FIO00-J. 共有ディレクトリにあるファイルを操作しない」を参照)。ファイルがセキュアなディレクトリのなかにあれば、攻撃者はファイルに対する操作を行えないため、競合状態を悪用した攻撃もできないことになる。
このルールは「IDS01-J. 文字列は検査するまえに標準化する」の具体例である。
違反コード
以下の違反コード例では、コマンドライン引数としてファイル名を受け取り、File.getAbsolutePath() メソッドを使って絶対パス名に変換している。さらに「FIO00-J. 共有ディレクトリにあるファイルを操作しない」で紹介した isInSecureDir() メソッドを使い、ファイルがセキュアなディレクトリのなかにあることを確認している。しかし、引数に含まれているかもしれないファイルリンクを解決しておらず、パス同一性の問題も残っている。
public static void main(String[] args) { File f = new File(System.getProperty("user.home") + System.getProperty("file.separator") + args[0]); String absPath = f.getAbsolutePath(); if (!isInSecureDir(Paths.get(absPath))) { throw new IllegalArgumentException(); } if (!validate(absPath)) { // パスの検証 throw new IllegalArgumentException(); } }
このコードでは、ユーザが自分のホームディレクトリの外にあるファイルを操作できないように制限することを意図している。パス名がホームディレクトリのなかのファイルを指していることを validate() メソッドを使って検証しようとしているが、これは容易に回避されてしまう。たとえば、ホームディレクトリ以下に、ホームディレクトリの外にあるファイルやディレクトリへのリンクを作ったとしよう。このリンク自身のパス名はホームディレクトリ以下にあるように見えるためチェックを通過するが、実際に操作されるファイルの実体はホームディレクトリの外にある。
Windows や Macintosh では、File.getAbsolutePath() はシンボリックリンク、エイリアス、ショートカットを解決してくれる。しかし Java 言語仕様では、この動作がすべての実行環境で同じであること、そして将来のバージョンでも同じであることのどちらも保証していない。
適合コード (getCanonicalPath())
以下の適合コードでは、Java 2 で導入された getCanonicalPath() メソッドを使っている。このメソッドは、すべての実行環境において、すべてのエイリアス、ショートカット、シンボリックリンクを解決する。ドット2つ(「..」)のような特殊ファイル名も削除されるため、入力は検証が行われる前に正規化形式に解決される。これにより、攻撃者は「../」のようなパターンを使ってディレクトリ外部にアクセスすることができなくなる。
public static void main(String[] args) throws IOException { File f = new File(System.getProperty("user.home") + System.getProperty("file.separator")+ args[0]); String canonicalPath = f.getCanonicalPath(); if (!isInSecureDir(Paths.get(canonicalPath))) { throw new IllegalArgumentException(); } if (!validate(canonicalPath)) { // パスの検証 throw new IllegalArgumentException(); } }
アプレットの中で getCanonicalPath() メソッドを呼び出すとセキュリティ例外が発生する。このメソッドが使えると、ホスト環境に関する情報を得ることができてしまうからだ。getCanonicalFile() メソッドは getCanonicalPath() メソッドと同様に動作するが、String ではなく、新しい File オブジェクトを返す。
適合コード (セキュリティマネージャ)
この問題に対するより包括的な対処法は、意図したディレクトリ(このコード例ではユーザのホームディレクトリ)のなかにあるファイルに対してのみ、アクセスを許可することである。以下に示すポリシー設定ファイルでは、ポリシーの適用対象となるプログラムを絶対パスで指定し、ターゲット ${user.home}/* に対してアクション read と write を指定した java.io.FilePermission を許可している。
grant codeBase "file:/home/programpath/" { permission java.io.FilePermission "${user.home}/*", "read, write"; };
この方法では、「FIO00-J. 共有ディレクトリにあるファイルを操作しない」で解説しているように、ユーザのホームディレクトリがセキュアなディレクトリであることが前提である。
違反コード
以下の違反コード例では、ユーザは操作したいファイルの絶対パス名を指定する。ユーザは、引数に「../ 」というパターンを含めることにより、想定しているディレクトリ(「/img」)の外のファイルを指定でき、プログラムが想定しているセキュリティポリシーを破ることが可能である。
FileOutputStream fis = new FileOutputStream(new File("/img/" + args[0])); // ...
違反コード
以下の違反コード例は、File.getCanonicalPath() メソッドを使って正規化パス名への変換を行うことで、上記の違反コード例の問題を解決しようとしている。たとえば、パス名 /img/../etc/passwd は /etc/passwd に変換される。しかし、正規化するだけでその結果を検証しないと、攻撃者は想定したディレクトリ外のファイルを指定できる。
File f = new File("/img/" + args[0]); String canonicalPath = f.getCanonicalPath(); FileOutputStream fis = new FileOutputStream(f); // ...
適合コード
以下の適合コードは、信頼できないユーザ入力からファイル名を取得し、それを正規化し、想定したパスであるかどうかを検証している。そして、検証に成功した場合のみ、特定のファイルを操作する。つまり、ファイルが /img/java配下にある2つの正当なファイル file1.txt もしくは file2.txt のいずれかである場合のみ検証は成功し、ファイル操作を行うことができる。
File f = new File("/img/" + args[0]); String canonicalPath = f.getCanonicalPath(); if (!canonicalPath.equals("/img/java/file1.txt") && !canonicalPath.equals("/img/java/file2.txt")) { // 無効なファイル、エラー処理を行う } FileInputStream fis = new FileInputStream(f);
競合状態を避けるためには、/img/java はセキュアなディレクトリである必要がある。
適合コード (セキュリティマネージャ)
以下の解決法は、アプリケーションに対して、プログラマが意図したファイルやディレクトリの読み取り操作のみを許可している。具体的には、セキュリティポリシ設定ファイルにプログラムの絶対パスを指定することで読取り権限を与え、操作対象となるファイルやディレクトリの正規化絶対パス名とそれらのread アクションを java.io.FilePermission に指定している。
// /img/java にあるファイルはすべて読取り可能 grant codeBase "file:/home/programpath/" { permission java.io.FilePermission "/img/java", "read"; };
リスク評価
はじめに正規化や検証を行うことなく、信頼できないソースから得たパス名を使用すると、ディレクトリトラバーサルやパス同一性の脆弱性につながる可能性がある。
ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
IDS02-J | 中 | 低 | 中 | P4 | L3 |
関連する脆弱性
CVE-2005-0789 は LimeWire (バージョン 3.9.6 から 4.6.0)のディレクトリトラバーサルの脆弱性である。magnet request に 「..」を含めることで、遠隔の第三者が任意のファイルを読み取ることができる。
CVE-2008-5518 は Apache Geronimo Application Server (2.1 から 2.1.3) の Windows 版に存在したディレクトリトラバーサルの脆弱性である。遠隔の第三者が任意のディレクトリにファイルをアップロードできてしまう。
関連ガイドライン
The CERT C Secure Coding Standard | FIO02-C. Canonicalize path names originating from untrusted sources |
The CERT C++ Secure Coding Standard | FIO02-CPP. Canonicalize path names originating from untrusted sources |
ISO/IEC TR 24772:2010 | Path Traversal [EWR] |
MITRE CWE | CWE-171. Cleansing, canonicalization, and comparison errors |
CWE-647. Use of non-canonical URL paths for authorization decisions |
参考文献
[API 2006] | method getCanonicalPath() |
[Harold 1999] |
翻訳元
これは以下のページを翻訳したものです。
IDS02-J. Canonicalize path names before validating them (revision 116)