マルチユーザシステムでは、権限の異なる複数のユーザが、ひとつのファイルシステムを共有することができる。そのような環境では、各ユーザは、共有するファイルとプライベートにするファイルを決め、その決定を実施できなくてはならない。
残念ながら、様々な種類のファイルシステムの脆弱性が攻撃者に悪用され、アクセス権限のないファイルに対してアクセスされてしまう。これは特に、複数のユーザがファイルの作成、移動、削除を行う共有ディレクトリ(shared directory)に置かれたファイルを、プログラムが操作する場合に問題になる。脆弱なプログラムが特権で動作する場合、権限昇格(privilege escalation)も可能になる。攻撃者によって悪用されるファイルシステムの特性や機能には、ファイルリンク、デバイスファイル、共有ファイルアクセスなどがある。脆弱性を防ぐには、プログラムはセキュアなディレクトリ(secure directory)に置かれたファイルに対してのみ操作するようにしなければならない。
あるユーザにとってディレクトリがセキュアであるとは、そのユーザとシステム管理者だけがディレクトリ内でファイルの作成、移動、削除を行える場合を指す。さらに、ルートディレクトリまでの各親ディレクトリもセキュアなディレクトリでなければならない。ほとんどのシステムの初期設定では、ホームディレクトリやユーザディレクトリがセキュアに設定されており、共有ディレクトリのみがセキュアでない。
ファイルリンク(File Links)
多くのオペレーティングシステムは、シンボリック(ソフト)リンク、ハードリンク、ショートカット、シャドウファイル、エイリアス、ジャンクション等のファイルリンクをサポートしている。POSIXシステムにおいて、シンボリックリンクは ln -s コマンド、ハードリンクはlnコマンドにより作成することができる。POSIXシステムにおいてはハードリンクと通常のファイルとの区別はない。
Windows の NTFS(New Technology File System)では、ハードリンク、ジャンクション、シンボリックリンクの3種類のファイルがサポートされている。シンボリックリンクは Windows Vista の NTFS で利用可能になった。
オープンしたファイルが別のファイルへのリンクである可能性を考慮していないプログラムにおいては、ファイルリンクがセキュリティ上の問題となりうる。これは特に、脆弱なプログラムが特権で実行されている場合に深刻な問題になる。ファイルの新規作成時、特権で動作するプログラムは共有ディレクトリの外に存在するファイルを誤って上書きしてしまうかもしれない。
デバイスファイル (Device Files)
多くのオペレーティングシステムでは、デバイスファイルのアクセスにファイル名が用いられる。デバイスファイルはハードウェアや周辺機器にアクセスするために用いられる。たとえばMS-DOSの予約デバイス名には、AUX, CON, PRN, COM1, LPT1 が存在する。キャラクタ特殊ファイル(character special file)やブロック特殊ファイル(block special file)等のPOSIXのデバイスファイルは、ファイル操作を適切なデバイスドライバに命令する。
通常のテキストファイルやバイナリファイルのみを想定したファイル操作を、デバイスファイルに対して実行すると、プログラムのクラッシュやサービス運用妨害(DoS)につながる。たとえば、Windowsがデバイス名をファイルリソースとして解釈しようとすると、不正なリソースアクセスを行ってしまう。これは通常プログラムのクラッシュを引き起こす。[Howard 2002]
POSIXにおけるデバイスファイルは、攻撃者が許可なくそれにアクセスできる場合にセキュリティ上のリスクとなる。たとえば、悪意のあるプログラムが/dev/kmemデバイスに対して読み書きできると、優先度(priority)、ユーザID、その他のプロセス属性を変更できたり、あるいは単純にシステムをクラッシュさせたりするかもしれない。同様に、他のプロセスが使用しているディスクデバイス、テープデバイス、ネットワークデバイス、ターミナルにアクセスされると問題が発生する[Garfinkel 1996]。
Linuxにおいては、ファイルではなくデバイスにデータを読み書きさせることで、アプリケーションをロックすることが可能である。以下のデバイスパス名について考えてみよう。
/dev/mouse /dev/console /dev/tty0 /dev/zero
ウェブブラウザがこれらのデバイスのチェックを怠っている場合、攻撃者はWebサイト上に <IMG src="file:///dev/mouse"> のようなイメージタグを置くことで、ユーザのマウスをロックできてしまう。
共有ファイルアクセス
多くのシステムでは、並行実行されるプロセスによってファイルが同時にアクセスされることがある。排他的(exclusive)アクセスは、ロックを持つプロセスが無制限にファイルにアクセスすることを許可する一方で、他のすべてのプロセスからのアクセスを拒否し、ファイルのロックされた領域において競合状態が発生する可能性を排除する。java.nio.channels.FileLock クラスを使用してファイルロックを行うことができる。Java API 仕様には以下のように記されている。[API 2006]
ファイルロックには「排他ロック」と「共有ロック」がある。共有ロックの場合、同時に実行されているその他のプログラムは、オーバーラップする排他ロックを獲得できない。オーバーラップする共有ロックであれば獲得可能である。一方、排他ロックの場合、どちらの種類のロックも獲得できない。ロックを解放すると、その他のプログラムによって獲得されるロックへの影響はなくなる。
共有ロックは複数のプロセスからの並行したリードアクセスをサポートし、排他ロックは排他的ライトアクセスをサポートする。ファイルロックは複数プロセス間の保護を行うが、単一プロセス内の複数スレッドには効果を発揮しない。共有ロックも排他ロックも、ロックを取得した領域において、プロセス間の競合状態の発生を防ぐ。排他ロックは相互排他(mutual exclusion)のメカニズムを提供し、共有ロックはロックされたファイル領域の状態変更を阻止する(データ競合に必要な属性)。
Java API 仕様 [API 2006] によると「ロックされた領域のコンテンツにその他のプログラムからアクセスできなくなるかどうかは、システムによって決まるため未規定である」。
Microsoft Windows は強制的ファイルロックのメカニズムを使用しており、ロックされた領域がプロセスによってアクセスされるのを防止している。
Linuxは強制ロック(mandatory lock)とアドバイザリロック(advisory lock)の両方を実装している。アドバイザリロックはOSから強制されないため、セキュリティ上の存在価値は低い。残念ながら、Linuxの強制ファイルロックは以下の理由から実用的ではない。
- 強制ロックは特定のネットワークファイルシステムでのみサポートされている
- ファイルシステムのマウント時に強制ロックのサポートを指定しなくてはならないが、これは初期設定では無効となっている
- ロックはグループIDビットに基づいて行われるが、これは他のプロセスによってオフにできる(ロックを無効化できてしまう)
- ロックを獲得しているプロセスが、ロックしているファイルのディスクリプタのどれかひとつを閉じると、強制ロックは暗黙的に無効にされる
違反コード
以下の違反コード例では、攻撃者はデバイスもしくはFIFOのファイル名を指定することができ、ファイルがオープンされるとプログラムはハングアップする。
String file = /* ユーザにより提供されている */; InputStream in = null; try { in = new FileInputStream(file); // ... } finally { try { if (in != null) {in.close();} } catch (IOException x) { // エラー処理 } }
違反コード (Java SE 7)
以下の違反コード例では Java SE 7 の "try-with-resources 文" を使用してファイルをオープンしている。これは、例外が投げられた際にファイルがクローズされることは保証するが、前述の例と同じ脆弱性の影響を受ける。
String filename = /* ユーザにより提供されている */; Path path = new File(filename).toPath(); try (InputStream in = Files.newInputStream(path)) { // ファイルをリードする } catch (IOException x) { // エラー処理 }
違反コード (Java SE 7: isRegularFile())
以下の違反コード例では、ファイルを開く前にまず、ファイルが通常ファイルであるかどうかを確認している(Java SE 7 の NIO2 APIを使用して)。
String filename = /* ユーザにより提供されている */; Path path = new File(filename).toPath(); try { BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class); // チェック if (!attr.isRegularFile()) { System.out.println("Not a regular file"); return; } // その他の必要なチェック // Use try (InputStream in = Files.newInputStream(path)) { // ファイルをリードする } } catch (IOException x) { // エラー処理 }
このコードで行っているテストは、シンボリックリンクにより回避されてしまう。 第3引数として別の挙動を指定しなければ、readAttributes() メソッドはシンボリックリンクをたどり、リンク先のファイルの属性を読み取る。結果として、プログラムは意図していないファイルを参照するかもしれない。
違反コード (Java SE 7: NOFOLLOW_LINKS)
以下の違反コード例では、NOFOLLOW_LINKS リンクオプションを付けて readAttributes() メソッドを呼び出してファイルをチェックすることで、シンボリックリンクの参照先をたどることを防止している。シンボリックリンクの参照先ではなくシンボリックリンク自身に対して isRegularFile() チェックを呼び出しているため、シンボリックリンクを検出することができる。
String filename = /* ユーザにより提供されている */ Path path = new File(filename).toPath(); try { BasicFileAttributes attr = Files.readAttributes( path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS ); // ファイルチェック if (!attr.isRegularFile()) { System.out.println("Not a regular file"); return; } // その他の必要なチェック // ファイルを使う try (InputStream in = Files.newInputStream(path)) { // ファイルをリードする }; } catch (IOException x) { // エラー処理 }
このコードは依然として TOCTOU 競合状態の影響を受ける。たとえば攻撃者は、ファイルのチェックが完了した後、そのファイルがオープンされる前に、通常ファイルをファイルリンクやデバイスファイルと置き換えることができる。
違反コード (Java SE 7: Check-Use-Check)
以下の違反コード例では、必要なチェックを行った後にファイルをオープンし、さらに、ファイルが移動されていないこと、オープンしたファイルがチェックしたファイルと同一であることを確認するために、2度目のチェックを行っている。こうすることで、ファイルのチェックとオープンの間に攻撃者がファイルを変更する機会を低減する。どちらのチェックでも、ファイルのfileKey属性を調べている。この属性は、ファイルを特定するための一意のキーの役割を果たしており、ファイルのパス名を使うよりも信頼できる。
Java SE 7 仕様[J2SE 2011]には fileKey について以下のように記されている。
fileKey は、与えられたファイルを一意に特定するオブジェクトもしくは, ファイルキーが存在しない場合は null を返す。プラットフォームやファイルシステムによっては、単一もしくは複数の識別子を組み合わせて使用することで、ファイルを一意に特定する。これらの識別子は、シンボリックリンクをサポートするファイルシステムや、単一のファイルが複数のディレクトリのエントリになれるようなファイルシステムにおいてファイルツリーを走査するような操作を行う際重要になる。たとえばUNIXファイルシステムでは、デバイスIDとi-node番号を組み合わせることでこれを実現する。
このメソッドが返すファイルキーが一意であることが保証されるのは、ファイルシステムとファイルが静的である場合のみである。ファイルの削除後に識別子がファイルシステムに再利用されるかどうかは実装依存であり、したがって未規定である。
このメソッドが返すファイルキーは、ファイルの同一性を検証するために比較してもよく、コレクションで使用するのに適している。ファイルシステムとファイルが静的であり、ファイルキーがnullでない2つのファイルが同一であるなら、それらのファイルキーは等しい。
String filename = /* ユーザにより与えられている */ Path path = new File(filename).toPath(); try { BasicFileAttributes attr = Files.readAttributes( path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS ); Object fileKey = attr.fileKey(); // ファイルのチェック if (!attr.isRegularFile()) { System.out.println("Not a regular file"); return; } // その他の必要なチェック // ファイルの使用 try (InputStream in = Files.newInputStream(path)) { // ファイルのチェック BasicFileAttributes attr2 = Files.readAttributes( path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS ); Object fileKey2 = attr2.fileKey(); if (fileKey != fileKey2) { System.out.println("File has been tampered with"); } // ファイルのリード }; } catch (IOException x) { // エラー処理 }
このコードでは、攻撃者が間違ったファイルを開かせることを防止するために長々とコードを連ねているが、依然として複数の脆弱性が存在する。
- 最初のファイルチェックとファイルオープンの間には TOCTOU 競合状態が存在する。この競合ウィンドウの間に、攻撃者は通常ファイルをシンボリックリンクや他の特殊ファイルに置き換えることができる。2度目のチェックはこの競合を検出するが、排除はできない。
- 攻撃者は通常ファイルをチェックさせ、特殊ファイルに置き換えてオープンさせ、さらに通常ファイルに置き換えて2度目のチェックをパスさせることでコードを攻略することができる。このような脆弱性が存在する理由は、Javaにはファイル名以外にファイルの属性を取得する方法が存在せず、ファイル名が使用される度に、ファイル名とファイルオブジェクトのバインディングが行われるからである。それゆえ、攻撃者はファイルをシンボリックリンクなど悪意あるファイルに置き換えることができる。
- ハードリンクを持つシステムでは、攻撃者は保護されたファイルへのハードリンクであるような悪意あるファイルを作成できてしまう。プログラムでハードリンクを検知することは難しく、パスの正規化を失敗させるために悪用される。この問題については、「IDS02-J. パス名は検証する前に正規化する」で詳しく解説する。
適合コード (POSIX, Java SE 7: セキュアなディレクトリ)
共有ディレクトリでは、本質的に他のユーザがファイルにアクセスでき、競合状態が発生する可能性がある。そのため、ファイル操作はセキュアなディレクトリの中でのみ行わなくてはならない。プログラムは権限を落として実行され、セキュアなディレクトリを作成できない可能性もあるため、与えられたパスがセキュアなディレクトリではないことが判断できるならば、例外を投げる必要があるかもしれない。
以下のコードに、isInSecureDir()メソッドのPOSIX固有の実装を示す。このメソッドは、与えられたファイルとそのすべての親ディレクトリがユーザ自身もしくはシステム管理者によって所有されていること、他のユーザにディレクトリの書き込み権限が与えられていないこと、与えられたファイルの親ディレクトリがシステム管理者を除く他のユーザによって削除したり名前を変更できないことを確認する。
public static boolean isInSecureDir(Path file) { return isInSecureDir( file, null); } public static boolean isInSecureDir(Path file, UserPrincipal user) { return isInSecureDir( file, null, 5); // symlink を辿る回数の最大値 } /** * プログラムのユーザからみて、ファイルがセキュアなディレクトリに存在するかどうかを示す * @param file テストするパス * @param user テストするユーザ. null ならばカレントユーザ * @param symlinkDepth アボートするまでに辿るシンボリックリンクの数 * @return true ディレクトリがセキュアであるかを示す */ public static boolean isInSecureDir(Path file, UserPrincipal user, int symlinkDepth) { if (!file.isAbsolute()) { file = file.toAbsolutePath(); } if (symlinkDepth <= 0) { // シンボリックリンクのレベルが深すぎる return false; } // 指定されたユーザとスーパユーザの UserPincipal を得る FileSystem fileSystem = Paths.get(file.getRoot().toString()).getFileSystem(); UserPrincipalLookupService upls = fileSystem.getUserPrincipalLookupService(); UserPrincipal root = null; try { root = upls.lookupPrincipalByName("root"); if (user == null) { user = upls.lookupPrincipalByName(System.getProperty("user.name")); } if (root == null || user == null) { return false; } } catch (IOException x) { return false; } // ルートディレクトリから下方向に辿ったときに、親ディレクトリのいずれか1つが // セキュアでなければ、ディレクトリはセキュアでない for (int i = 1; i <= file.getNameCount(); i++) { Path partialPath = Paths.get(file.getRoot().toString(), file.subpath(0, i).toString()); try { if (Files.isSymbolicLink(partialPath)) { if (!isInSecureDir(Files.readSymbolicLink(partialPath), user, symlinkDepth - 1)) { // シンボリックリンクされた先のディレクトリがセキュアでない return false; } } else { UserPrincipal owner = Files.getOwner(partialPath); if (!user.equals( owner) && !root.equals( owner)) { // 他のユーザが所有するディレクトリ, セキュアでない return false; } PosixFileAttributes attr = Files.readAttributes(partialPath, PosixFileAttributes.class); Set<PosixFilePermission> perms = attr.permissions(); if (perms.contains(PosixFilePermission.GROUP_WRITE) || perms.contains(PosixFilePermission.OTHERS_WRITE)) { // 他のユーザがファイルに書き込める, セキュアでない return false; } } } catch (IOException x) { return false; } } return true; }
ディレクトリをチェックする場合、ルートから葉に向かってツリーを走査し、危険な競合状態を回避することが重要である。競合状態が存在すると、攻撃者は、サブディレクトリの権限が検証された後であり、かつ変更したディレクトリが検査される前に、自分がアクセス権限を持つディレクトリの名前を変更し、ディレクトリの構成を変更することができる。
パスにシンボリックリンクが含まれる場合、このメソッドはリンク先のディレクトリに対して再帰的に呼び出され、セキュアであることを確認する。シンボリックリンクは、そのリンク元とリンク先のディレクトリがどちらもセキュアである場合、セキュアであるといえるであろう。このメソッドは、パス中のすべてのディレクトリをチェックし、すべてのディレクトリが現在のユーザもしくはシステム管理者によって所有されていること、他のユーザがファイルの作成と削除、名前の変更ができないことを確認する。
POSIXシステムにおいては、グループおよびその他のユーザに対してディレクトリへのアクセスを無効にすれば、ディレクトリの所有者とシステム管理者以外のユーザがディレクトリを変更することを防げる。
このメソッドは、POSIXのパーミッションと完全に互換性のあるファイルシステムでのみ有効であり、他のパーミッション体系に基づくファイルシステムでは正しく動作しないであろう。
以下の解決法では、isInSecureDir()メソッドを使用することで、オープンし削除されるファイルを攻撃者がいじれないことを確認している。isInSecureDir()を使用してひとたびディレクトリのパス名をチェックしたなら、そのディレクトリに対するファイル操作はすべて同じパスを使用して行わなくてはならない。このコードは前述のコード例と同様のチェックを行っており、ファイルが通常のファイルであって、シンボリックリンクやデバイスファイル、その他の特殊ファイルでないことも確認している。
String filename = /* ユーザにより与えられている */; Path path = new File(filename).toPath(); try { if (!isInSecureDir(path)) { System.out.println("File not in secure directory"); return; } BasicFileAttributes attr = Files.readAttributes( path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); // ファイルのチェック if (!attr.isRegularFile()) { System.out.println("Not a regular file"); return; } // その他の必要なチェック try (InputStream in = Files.newInputStream(path)) { // ファイルのリード } } catch (IOException x) { // エラー処理 }
特権を与えられたプログラムは、権限を持たないユーザが所有するディレクトリにファイルを書き出さなくてはならないかもしれない。このようなプログラムの一例であるメールデーモンでは、あるユーザから送られたメールメッセージを読み取り、他のユーザが所有するディレクトリにそれを保存する。そのような場合、メールデーモンは、ユーザの権限でファイルの読み書きを行い、そのユーザからみてセキュアなディレクトリにおいてすべてのファイルアクセスを行う。プログラムが元々与えられた特権でファイルを書き出さなくてはならない場合、ファイルはその特権におけるセキュアなディレクトリに置かれなくてはならない(たとえば、システム管理者のみがアクセスできるディレクトリなど)。
例外
FIO00-EX0: シングルユーザシステム、共有ディレクトリが存在しないシステム、ファイルシステムに脆弱性が存在しないシステムで動作するプログラムは、ファイルを操作する前にファイルがセキュアなディレクトリにあることを確認する必要はない。
リスク評価
共有ディレクトリに置かれたファイルの操作を許すと、サービス運用妨害攻撃につながる恐れがある。プログラムに特権が付与されている場合、権限昇格攻撃が可能になることも考えられる。
ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
FIO00-J | 中 | 低 | 中 | P4 | L3 |
関連ガイドライン
CERT C Secure Coding Standard | FIO32-C. Do not perform operations on devices that are only appropriate for files |
CERT C++ Secure Coding Standard | FIO32-CPP. Do not perform operations on devices that are only appropriate for files |
MITRE CWE | CWE-67. Improper handling of windows device names |
参考文献
[API 2006] | Class File, methods createTempFile, delete, deleteOnExit |
[CVE 2011] | CVE-2008-5354 |
[Darwin 2004] | 11.5 Creating a Transient File |
[Garfinkel 1996] | Section 5.6, Device Files |
[Howard 2002] | Chapter 11, "Canonical Representation Issues" |
[J2SE 2011] | The try-with-resources Statement |
[Open Group 2004] | open() |
[SDN 2008] | Bug IDs: 4171239, 4405521, 4635827, 4631820 |
[Secunia 2008] | Secunia Advisory 20132 |
翻訳元
これは以下のページを翻訳したものです。
FIO00-J. Do not operate on files in shared directories (revision 61)