FIO21-C. 一時ファイルを共有ディレクトリに作成しない
プログラマは誰でも書き込めるディレクトリに一時ファイルを作成することがよくある。このようなディレクトリの例としては、UNIX の /tmp や /var/tmp、Windows の %TEMP% などがある。このようなディレクトリに置かれた一時ファイルは、たとえば毎晩あるいは再起動時に定期的に消去される。
一時ファイルは一般に、メモリ上に展開する必要のない、あるいはそれが不可能なデータのための補助的な保管場所として利用されたり、ファイルシステムを介して他のプロセスとの間でデータ転送を行うための手段として利用される。たとえば、一方のプロセスが共有ディレクトリに決まったファイル名あるいは仮のファイル名で一時ファイルを作成し、他方のプロセスにファイル名を連絡する。このように作成されたファイルを使うことで、プロセス間で情報を共有することができる。
このようなやり方は危険である。なぜなら、決まった名前で共有ディレクトリに置かれているファイルは、攻撃者によって容易に乗っ取られたり改変されたりする恐れがあるからだ。この脅威を緩和する方法として、下記の方法が考えられる。
- ソケットや共有メモリなど、他の低レベルなプロセス間通信 (IPC) の仕組みを利用する。
- リモートプロシージャコールなど、高レベルな IPC の仕組みを利用する。
- セキュアなディレクトリや jail 環境を利用し、意図したアプリケーションだけがアクセスできるようにする(同一プラットフォーム上でアプリケーションが複数実行されている場合、それらの間で競合が発生しないようにする)。
プロセス間通信(IPC)のメカニズムにはさまざまな種類があり、一時ファイルを要するものもあれば、そうでないものもある。一時ファイルを利用する IPC にはたとえば POSIX の mmap() 関数がある。バークレイソケットや POSIX Local IPC ソケット、System V 共有メモリは一時ファイルを必要としない。共有ディレクトリは、複数のユーザが利用することから、セキュリティリスクを伴う。したがって、IPC で共有一時ファイルを利用することは推奨されない。
1つのディレクトリに対して複数のユーザまたはユーザグループが書き込み可能な状況は、少数のファイルが共有されている状況よりも、問題が発生する可能性がはるかに高くなる。それゆえ、共有ディレクトリに置かれる一時ファイルは以下の条件を満たすべきである。
- 予測できないファイル名で作成される
- 一意なファイル名で作成される
- ファイルが既に存在しない場合にのみオープンされる (atomic open)
- 排他的アクセスでオープンされる
- 適切なパーミッションでオープンされる
- プログラムの終了前に削除される
一時ファイルを作成する関数について、前述の条件への適合性を次の表に示す。
tmpnam (C) |
tmpnam_s (附属書 K) |
tmpfile (C) |
tmpfile_s (附属書 K) |
mktemp (POSIX) |
mkstemp (POSIX) |
|
---|---|---|---|---|---|---|
予測不能なファイル名 | 可搬性なし | Yes | 可搬性なし | Yes | 可搬性なし | 可搬性なし |
一意のファイル名 | Yes | Yes | Yes | Yes | Yes | Yes |
アトミックなファイルオープン | No | No | Yes | Yes | No | Yes |
排他的アクセス | 可能 | 可能 | No | OS がサポートしている場合 | 可能 | OS がサポートしている場合 |
適切なパーミッション | 可能 | 可能 | No | OS がサポートしている場合 | 可能 | 可搬性なし |
ファイルを削除する | No | No | Yes* | Yes* | No | NoO |
* プログラムが異常終了した場合、その動作は処理系定義である。
安全に一時ファイルを作成する操作には間違いが伴いやすく、操作は、使用する C ランタイムライブラリのバージョンやオペレーティングシステム、ファイルシステムの影響も受ける。 たとえば、ローカルにマウントされたファイルシステムで動作するコードは、ネットワークを介してマウントされたファイルシステムで使用すると脆弱性を発現する可能性がある。 また、これらすべての関数にはなんらかの問題が伴う。 唯一安全な解決策は、一時ファイルを共有ディレクトリに作成しないことである。
一意かつ予測不可能なファイル名
誰もが書き込めるディレクトリに一時ファイルを作成する特権プログラムが悪用されると、保護されたシステムファイルを上書きする可能性がある。 特権プログラムが作成するファイルの名前を予測できれば、攻撃者は、特権プログラムが使用するファイルと同じファイル名で、保護されたシステムファイルを指すシンボリックリンクを作成することができる。 特権プログラムが安全にコーディングされていなければ、プログラムは、本来使用すべきファイルを開いたり作成したりする代わりに、そのシンボリックリンクをたどる。 その結果、プログラムが実行されると、シンボリックリンクが指す保護されたシステムファイルが上書きされてしまう可能性がある[HP 2003]。 同様に、特権のないプログラムも、保護されたユーザファイルを上書きするために悪用されることがある。
排他的アクセス
排他的アクセスにより、ロックを取得したプロセスに対しては無制限のファイルアクセスを許す一方で、他の全てのプロセスのアクセスを拒否し、ロックされているファイル領域で競合状態が発生する可能性を排除する。(『C/C++ セキュアコーディング』第8章を参照 [Seacord 2013])。
ファイルまたはファイルの一部をロックすることで、2つのプロセスがファイルに並行アクセスすることを防ぐことができる。Windows は2種類のファイルロック機構を提供している。
- 共有ロック: LockFile() が提供する仕組みであり、ロックされているファイル領域への書き込みアクセスをすべて禁止する一方で、すべてのプロセスに対して読み取りアクセスを許可する。
- 排他的ロック: LockFileEx() が提供する仕組みであり、ロックを取得したプロセスには無制限アクセスを許可する一方で、他のすべてのプロセスに対してはアクセスを拒否する。
どちらの場合も UnlockFile() の呼出しによりロックが解除される。
共有ロックも排他的ロックも、ロックされた領域において競合状態が発生する可能性を排除する。排他的ロックは排他制御に似た仕組みであり、共有ロックはロックされているファイル領域の状態が改変される可能性を取り除くことで、競合状態を排除する (競合が発生するための条件の1つ)。
これら Windows のファイルロック機構は、強制ロックと呼ばれる。その理由は、ロックされているファイル領域にアクセスしようとする全てのプロセスが制限を受けるからである。Linux は強制ロックとアドバイザリロックの2つを実装している。アドバイザリロックは OS によって強制されないため、セキュリティ上の価値は大きく減じられている。また、次に挙げる理由から Linux の強制ファイルロックは現実的ではない。
- 強制ロックはローカルファイルシステムでのみ機能し、ネットワークファイルシステム (NFS や AFS など) では機能しない。
- ファイルシステムは強制ロックがサポートされた状態でマウントする必要があるが、デフォルトでは無効になっている。
- ロックはグループ ID ビットに基づいて行われるが、別のプロセスがこれをオフにすることができる (これによりロックが無効になる)。
終了前の削除
不要になった一時ファイルを削除することで、ファイル名や二次記憶領域などの資源を再利用することができる。プログラムの異常終了時に、一時ファイルを確実に消去する方法はない。そのため、一時ファイル削除ユーティリティが広く利用されている。削除ユーティリティは、システム管理者が手動で起動したりデーモンによって定期的に実行され、一時ファイル用ディレクトリから古いファイルを削除する。しかし、このようなユーティリティプログラム自身、ファイル操作に関連する攻撃を受けやすく、また共有ディレクトリを必要とすることも多い。プログラムは責任をもって、一時ファイルを確実に削除するか、あるいは tmpfile_s() などのライブラリ関数を使ってプログラム終了時に一時ファイルが削除されることを保証すべきである。
違反コード (tmpnam() を使用した fopen()/open())
以下のコード例では、ハードコードされた file_name を使ってファイルを作成している (/tmp や C:\Temp などの共有ディレクトリ内で行われていると仮定する)。
#include <stdio.h> void func(const char *file_name) { FILE *fp = fopen(file_name, "wb+"); if (fp == NULL) { /* エラー処理 */ } }
ファイル名はハードコードされている。つまり一意でも予測不能でもない。このため、攻撃者がファイルをシンボリックリンクに置きかえさえすれば、シンボリックリンクが参照する対象のファイルが開かれ、内容が消去されてしまう。
下の違反コードは、tmpnam() 関数を使い実行時にファイル名を生成することで、問題を修正しようとしている。C 標準ライブラリの tmpnam() 関数は、ファイル名として有効かつ既存のファイル名と一致しない文字列を生成する。tmpnam() 関数が生成した文字列を使って作成したファイルは、処理系の一般的な命名規則に従って生成された名前と衝突しえないという点において、一時ファイルの性質を持つ。tmpnam() 関数は TMP_MAX 個の異なる文字列を生成できるが、既存のファイルにそれらのすべてまたは一部が既に使用されているかもしれない。
#include <stdio.h> void func(void) { char file_name[L_tmpnam]; FILE *fp; if (!tmpnam(file_name)) { /* エラー処理 */ } /* A TOCTOU race condition exists here */ fp = fopen(file_name, "wb+"); if (fp == NULL) { /* エラー処理 */ } }
tmpnam() が一意のファイル名を返す保証はなく、fopen() はファイルを排他的にオープンすることができないため、このコードは依然として脆弱である。
次の違反コード例では、POSIX の open() 関数を使って既存のファイルが開かれているか新しいファイルが作成されているかを検査することで問題の解決を図っている [IEEE Std 1003.1:2013]。O_CREAT および O_EXCL フラグを同時に使用すること、file_name が指定するファイルがすでに存在する場合に open() 関数は失敗する。既存のファイルを開いて内容を消去してしまうことを防止するには、open() を呼び出す時に O_CREAT と O_EXCL を指定する。
#include <stdio.h> void func(void) { char file_name[L_tmpnam]; int fd; if (!(tmpnam(file_name))) { /* エラー処理 */ } /* A TOCTOU 競合状態がここに存在する */ fd = open(file_name, O_WRONLY | O_CREAT | O_EXCL | O_TRUNC, 0600); if (fd < 0) { /* エラー処理 */ } }
file_name (シンボリックリンクである場合も含む) が既に存在すると open() の呼び出しは失敗するが、依然として一時ファイルは必要とされる。さらに、tmpnam() が生成するファイル名は予測不可能であることは保証されておらず、新たに生成するファイル名を攻撃者が事前に推測する可能性は残る。
O_EXCL をネットワークファイルシステムで使用する場合は注意が必要である。O_EXCL は NFS バージョン 2 では動作しないが、NFS バージョン 3 は open() で O_EXCL をサポートした。詳しくは、IETF RFC 1813 [Callaghan 1995]、特に CREATE の mode 引数への EXCLUSIVE 値を参照のこと。
さらに、Standard for Information Technology—Portable Operating System Interface (POSIX®), Base Specifications, Issue 7 が規定する open() 関数は、共有ロックや排他的ロックをサポートしていない [IEEE Std 1003.1:2013]。しかし、BSD システムでは、これらのロックを使用するための下記の2つのフラグをサポートしている。
- O_SHLOCK 共有ロックをアトミックな操作で取得する。
- O_EXLOCK 排他的ロックをアトミックな操作で取得する。
違反コード (tmpnam_s()/open(), 附属書 K, POSIX)
C 標準関数 tmpnam_s() は、ファイル名として有効でありかつ既存のファイル名と一致しない文字列を生成する。バッファサイズを指定するための maxsize 引数以外は、前述の tmpnam 関数とほぼ同じである。
#define __STDC_WANT_LIB_EXT1__ #include <stdio.h> void func(void) { char file_name[L_tmpnam_s]; int fd; if (tmpnam_s(file_name, L_tmpnam_s) != 0) { /* エラー処理 */ } /* A TOCTOU 競合状態がここに存在する */ fd = open(file_name, O_WRONLY | O_CREAT | O_EXCL | O_TRUNC, 0600); if (fd < 0) { /* エラー処理 */ } }
C言語規格の規定外 (nonnormative) に位置づけられる K.3.5.1.2 項では、下記のことを推奨している。
処理系は tmpnam_s 関数が返すファイル名に使用するパターンの選択に注意すべきである。たとえば、同一のユーザによって実行される複数のプログラムが同じ一時ファイル名を生成するとき、スレッド ID をファイル名の一部に使用することで、競合状態や名前の衝突を防ぐことができる。
もしこのように実装した場合、ファイル名を一意にするための領域は小さくなり、ファイル名が予測される可能性は高まる。一般に、付属書 K は名前の予測可能性に関する基準も設けていない。たとえば、Microsoft Visual Studio の tmpnam_s 関数が生成するファイル名は、プログラムが生成したファイル名と、(tmpnam_s() を最初に呼び出したあとは) 基数 32 の連番によるファイル拡張子から構成される (.1〜.1vvvvvu)。
違反コード (mktemp()/open(), POSIX)
POSIX 関数 mktemp() は、与えられたファイル名テンプレートを使用し、その一部を上書きしてファイル名を生成する。 テンプレートは、ファイル名に X を6文字付け加えた形の文字列である (たとえば /tmp/temp.XXXXXX)。末尾の X は現在のプロセス番号と一意の文字の組み合わせで置き換えられる。
#include <stdio.h> #include <stdlib.h> void func(void) { char file_name[] = "tmp-XXXXXX"; int fd; if (!mktemp(file_name)) { /* エラー処理 */ } /* A TOCTOU 競合状態がここに存在する */ fd = open(file_name, O_WRONLY | O_CREAT | O_EXCL | O_TRUNC, 0600); if (fd < 0) { /* エラー処理 */ } }
Open Group Base Specifications Issue 6 では、mktemp() 関数は「レガシー」であると記されている [Open Group 2004]。mktemp() のマニュアルには次のように詳細が記されている。
決して mktemp() を使用しないこと。一部の実装では、BSD 4.3 に従い、XXXXXX を現在のプロセス ID とさらに1文字で置き換えることで、最大26個の異なる名前を返す。ファイル名は推測しやすく、かつ、ファイルの存在チェックとファイルオープンの間に競合が発生するため、mktemp() の使用はすべからくセキュリティ上のリスクとなる。競合は mkstemp(3) を使うことで防ぐことができる。
違反コード (tmpfile())
tmpfile() 関数は、既存のどのファイル名とも衝突しない一時ファイルをバイナリモードで作成する。作成された一時ファイルは、ファイルが閉じられたとき、あるいは、プログラムの終了時に自動的に削除される。
プログラムの実行中は、少なくとも TMP_MAX 個の一時ファイルを開くことができなければならない (この制限は、tmpnam() と共通であるかもしれない)。C標準規格 7.21.4.4項、第6パラグラフ では、マクロ TMP_MAX の値を最低 25 という小さな数に設定することを許している。
過去の多くの実装は、一時ファイル名の数はごく小さなものであり(通常 26)、それを超えるとファイル名は再利用された。
#include <stdio.h> void func(void) { FILE *fp = tmpfile(); if (fp == NULL) { /* エラー処理 */ } }
違反コード (tmpfile_s(), 附属書 K)
tmpfile_s() 関数は、既存のどのファイルとも衝突しない一時ファイルをバイナリモードで作成する。一時ファイルは、ファイルが閉じられるかプログラムが終了したとき、自動的に削除される。プログラムが異常終了した場合、一時ファイルが削除されるかどうかは処理系定義である。
一時ファイルは、更新用に "wb+" モードで開かれる。"wb+" は「バイナリファイルを更新モードで生成するか、または長さゼロに切り捨てる」という意味である。システムがこの概念をサポートしていれば、一時ファイルは、排他的に (共有されることなく) 開かれ、システムの他のユーザがアクセスできないファイルパーミッションを持つ。
プログラムの実行中は、少なくとも TMP_MAX_S 個の一時ファイルを開くことができなければならない (この制限は tmpnam_s() と共通かもしれない)。マクロ TMP_MAX_S の値は、25 以上であることだけが要求されている[ISO/IEC 9899:2011]。
C言語規格、K3.5.1.2項、第7パラグラフには、tmpnam_s() の代わりに tmpfile_s() を使用することが言及されている [ISO/IEC 9899:2011]。
プログラムが tmpnam_s を使用してファイル名を取得し、その名前でファイルを作成する前に、別の誰かが同じ名前でファイルを作成する可能性がある。このような競合状態を避けるために、可能な場合は tmpnam_s ではなく tmpfile_s 関数を使用すべきである。tmpnam_s 関数を使用する必要がある状況としては、一時ファイルではなく一時ディレクトリを作成する必要がある場合が挙げられる。
#define __STDC_WANT_LIB_EXT1__ #include <stdio.h> void func(void) { FILE *fp; if (tmpfile_s(&fp)) { /* エラー処理 */ } }
一時ファイルを /tmp/ や C: のような共有ディレクトリに作成する処理系では、tmpfile_s() 関数を使うべきでない。なぜならば、一時ファイルを作成するディレクトリをユーザが指定できないからである。
適合コード (mkstemp(), POSIX)
mkstemp() がファイル名を生成するアルゴリズムは攻撃に耐性があることが知られている。mkstemp() 関数は、Open Group Base Specifications Issue 4、バージョン 2 以降をサポートするシステムで利用することができる。
mkstemp() を呼び出すと、テンプレート文字列内の 6 つの X が、ランダムに選択された6つの文字に置き換えられ、(読み取りおよび書き込みのためにオープンされた)ファイルのファイル記述子を返す。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> extern int secure_dir(const char *sdn); void func(void) { const char *sdn = "/home/usr1/"; char sfn[] = "/home/usr1/temp-XXXXXX"; FILE *sfp; if (!secure_dir(sdn)) { /* エラー処理 */ } int fd = mkstemp(sfn); if (fd == -1) { /* エラー処理 */ } /* * ただちにアンリンクしてファイル名を隠す * ファイルを排他的パーミッションで作成していれば * 競合状態は発生しない (glibc >= 2.0.7). */ if (unlink(sfn) == -1) { /* エラー処理 */ } sfp = fdopen(fd, "w+"); if (sfp == NULL) { close(fd); /* エラー処理 */ } /* 一時ファイルを使用する */ fclose(sfp); /* fd も閉じる */ }
この修正例は、mkstemp() 関数が最初に呼び出されたときに、テンプレートの "XXXXXX" が置き換えられるため、使い回しが効かない。mkstemp() を再び呼び出す前にテンプレートを再初期化する限り、これは問題にはならない。テンプレートを再初期化していない場合、テンプレートに 6 つの X がないため、mkstemp() 関数は -1 を返しテンプレートを変更しない。
Open Group Base Specifications Issue 6 [Open Group 2004] では、作成されるファイルのパーミッションを定めておらず、処理系定義である。しかし、IEEE Std 1003.1, 2013 Edition [IEEE Std 1003.1:2013] では、作成されるファイルのパーミッションは S_IRUSR|S_IWUSR (0600) であることを定めている。
この修正コードでは、ユーザ定義の secure_dir() 関数を呼び、一時ファイルがセキュアディレクトリに作成されることを確認している (たとえば「FIO15-C. ファイル操作はセキュアディレクトリで行なう」で定義されているように)。
処理系固有の詳細
glibc バージョン 2.0.6 以前の場合、ファイルはパーミッション 0666 で作成される。glibc バージョン 2.0.7 以降の場合、ファイルはパーミッション 0600 で作成される。NetBSD では、ファイルはパーミッション 0600 で作成される。ファイルがパーミッション 0666 で作成される場合、ファイル作成直後には攻撃者がファイルへの書き込みアクセス権を持つので、セキュリティ上のリスクが発生する。その場合には、この問題が解決されているバージョンの mkstemp() 関数を使う必要がある。
多くの古い実装では、ファイル名はプロセス ID と時刻をもとに生成されるため、攻撃者は事前にファイル名を推測して罠を仕掛けることができる。FreeBSD は mk*temp() 関数群を変更し、作成されるファイル名からプロセス ID 部分を取り除き、フィールド全体を基数 62 でエンコードしたランダムな値に置き換えるようになった。これにより、テンプレートに6つの X を含めた典型的な使い方をした場合の一時ファイル名の数が大幅に増加した。これはつまり、6つの X を含むテンプレートで mktemp() を呼び出しても、関数を頻繁に使用しない限り、推測される確率を十分抑えられ、安全であることを意味する [Kennaway 2000]。
例外
FIO21-EX1: プログラムの実行環境で一時ファイルがセキュアディレクトリに作成されるならば、附属書 K の tmpfile_s() 関数を利用してもよい。
リスク評価
一時ファイルを安全に作成できない場合、ローカルシステム上で意図しないファイルがアクセスされたり、パーミッションが変更されてしまう可能性がある。
ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
FIO21-C | 高 | 中 | 中 | P12 | L1 |
自動検出(最新の情報はこちら)
ツール |
バージョン |
チェッカー |
説明 |
---|---|---|---|
CodeSonar |
4.0 |
BADFUNC.TEMP.* |
一時ファイルの脆弱性に関連するライブラリ関数の使用を通知する一連のチェック |
Compass/ROSE |
|
|
このレコメンデーションの違反を検知することができる。特に tmpnam(), tmpnam_s(), tmpfile(), mktemp() の使用を検出する |
Coverity |
6.5 |
SECURE_TEMP |
完全に実装されている |
LDRA tool suite |
8.5.4 |
489 S |
部分的に実装されている |
PRQA QA-C |
v8.2 |
warncall tmpnam, tmpfile, mktemp, tmpnam_s |
部分的に実装されている |
関連するガイドライン
CERT C Secure Coding Standard | FIO15-C. Ensure that file operations are performed in a secure directory |
CERT C++ Secure Coding Standard | FIO43-CPP. Do not create temporary files in shared directories |
CERT Oracle Secure Coding Standard for Java | FIO03-J. Remove temporary files before termination |
CERT Oracle Secure Coding Standard for Java | FIO03-J. Remove temporary files before termination |
ISO/IEC TR 24772:2013 | Path Traversal [EWR] |
MITRE CWE | CWE-379, Creation of temporary file in directory with insecure permissions |
参考資料
[HP 2003] | |
[IEEE Std 1003.1:2013] | XSH, System Interfaces: open XSH, System Interfaces: mkdopen, mksopen |
[ISO/IEC 9899:2011] | Subclause K.3.5.1.2, "The tmpnam_s Function" Subclause 7.21.4.4, "The tmpnam Function" |
[Kennaway 2000] | |
[Open Group 2004] | mkstemp() mktemp() open() |
[Seacord 2013] | Chapter 3, "Pointer Subterfuge" Chapter 8, "File I/O" |
[Viega 2003] | Section 2.1, "Creating Files for Temporary Use" |
[Wheeler 2003] | Chapter 7, "Structure Program Internals and Approach" |
翻訳元
これは以下のページを翻訳したものです。
FIO21-C. Do not create temporary files in shared directories (revision 177)