FIO05-C. 複数のファイル属性を使用してファイルを特定する
ファイルを特定するには、多くの場合、ファイルの所有者や作成時刻を比較するなど、ファイル名以外の属性を利用できる。作成したファイルに関する情報を保存し、再度開く際にその情報を使用してファイルの同一性を確かめることができる。
ファイルを再度オープンするとき、複数のファイル属性を比較することで、 より確実に同じファイルをオープンすることができる。
ファイルの所有者と(おそらく)システム管理者だけがアクセスできるセキュアなディレクトリにファイルを保存していれば、ファイルの識別はそれほど問題にならない(「FIO15-C. ファイル操作はセキュアなディレクトリで行われることを保証する」を参照)。
違反コード(再オープン)
以下のコード例は、ファイルを書き込み用に開いて閉じ、同じファイルを再度読み取り用に開いて閉じる。このコードは、ファイル名だけを使用してファイルを識別している。
char *file_name; /* file_name を初期化 */ FILE *fd = fopen(file_name, "w"); if (fd == NULL) { /* エラー処理 */ } /*... ファイルへの書き出し ...*/ fclose(fd); fd = NULL; /* * 競合状態が生じると、攻撃者は * ファイルを別なファイルに入れ替えることができる */ /* ... */ fd = fopen(file_name, "r"); if (fd == NULL) { /* エラー処理 */ } /*... ファイルからの読み取り ...*/ fclose(fd); fd = NULL;
読み取り用に開いたファイルが、書き込み用に開いたものと同じであるという保証はない。攻撃者は最初の fclose() から 2 回目の fopen() までの間に、元のファイルを(たとえばシンボリックリンクに)差し替えることができる。
適合コード (POSIX) (デバイス/iノード)
一般に、ファイルストリームの再オープンは避けるべきである。ただし、長時間実行されるアプリケーションにおいては、使用可能なファイル記述子の枯渇を避けるために、再オープンが必要な場合がある。
以下の解決法では、「検査、使用、検査」というパターンを使用して、読み取り用に開いたファイルが、書き込み用に開いたものと同じであることを確認している。この解決法は open() 関数を使用して書き込み用にファイルを開く。ファイルが正常に開いたら、fstat() 関数を使用してファイルに関する情報を読み取り、orig_st 構造体に保存する。このファイルを読み取り用に再度開くときに、ファイルの情報を new_st 構造体に読み込み、orig_st と new_st の st_dev と st_ino のフィールドを比較することで、同一性の判別を向上させている。
struct stat new_st; char *file_name; /* file_name を初期化 */ int fd = open(file_name, O_WRONLY); if (fd == -1) { /* エラー処理 */ } /*... ファイルへの書き出し ...*/ if (fstat(fd, &orig_st) == -1) { /* エラー処理 */ } close(fd); fd = -1; /* ... */ fd = open(file_name, O_RDONLY); if (fd == -1) { /* エラー処理 */ } if (fstat(fd, &new_st) == -1) { /* エラー処理 */ } if ((orig_st.st_dev != new_st.st_dev) || (orig_st.st_ino != new_st.st_ino)) { /* ファイルが不正操作されている! */ } /*... ファイルからの読み取り ...*/ close(fd); fd = -1;
これによって、最初の close() から 2 回目の open() までの間に攻撃者がファイルを差し替えた場合は、プログラムによって検出することができる。ただし、ファイルが元の場所で変更を加えられた場合は、プログラムで検出することはできない。
別の方法として、C99 の fopen() 関数を使用してファイルを開き、POSIX の fileno() 関数を使用して FILE ポインタをファイル記述子に変換することで、同じ解決法を実装することもできる。
POSIX 適合システムでは、構造体メンバ st_mode、st_ino、st_dev、st_uid、st_gid、st_atime、st_ctime、st_mtime にすべて、有意の値が設定されている。st_ino フィールドには、ファイルシリアル番号が格納される。st_dev フィールドは、ファイルが格納されているデバイスを示す。st_ino および st_dev フィールドを組み合わせることで、ファイルは一意に識別される。しかし、st_dev には再起動やシステムクラッシュのあとは必ずしも同じ値が維持されているとは限らないため、ファイルを再度開く前にシステムクラッシュや再起動が発生する可能性がある場合は、ファイルの識別用にこのフィールドを使用することはできない。
ファイル名を対象に stat() を呼び出してから open() を呼び出すのではなく、すでに開いているファイルに対して fstat() を呼び出すことで、開きたいファイルと、開いているファイルが確実に同じであることを保証する。識別にファイル名を使用することで生じる競合状態を避けるための情報については、「FIO01-C. ファイル名を使用してファイルを識別する関数の使用に注意する」を参照すること。
また、「FIO32-C. 通常ファイルに固有の操作をデバイスファイルに対して行わない」にあるように、特殊なファイルを開くことによるプログラムのハングを防ぐためにO_NONBLOCK を指定して open() を呼び出すことが必要な場合もある。
この解決法は、いくつかのケースでは効果がない場合がある。たとえば、長期的に実行しているサービスはログメッセージを追加するために、ときどきログファイルを再度開くが、定期的に循環できるようにファイルは閉じておくことがある。この場合は、inode 番号が変更されるため、この解決法は適用できない。
適合コード (POSIX) (一度だけ開く)
簡単な解決法はファイルを再度開かないことである。以下の解決法では、ファイルを読み書き用に一度だけ開いている。書き込みが終了したら、fseek() 関数でファイルポインタをファイルの先頭に移し、内容をもう一度読んでいく(「FIO07-C. rewind() ではなく fseek() を使用する」を参照)。
ファイルを再度開かないため、書き込みのあとの読み取りまでに、攻撃者がファイルを改ざんする可能性がなくなる。
char *file_name; FILE *fd; /* file_name を初期化 */ fd = fopen(file_name, "w+"); if (fd == NULL) { /* エラー処理 */ } /*... ファイルへの書き出し ...*/ /* ファイルの先頭へ移動 */ fseek(fd, 0, SEEK_SET); /*... ファイルからの読み取り ...*/ fclose(fd); fd = NULL;
「FIO39-C. fflush 関数や位置付け関数を呼び出さずにストリームへの入出力を交互に行わない」に従って、データの書き込み後は必ず fflush() を呼び出すこと。
違反コード (ファイルの所有者である場合)
オペレーティングシステムによるアクセス制御のもとでは、プロセスの「実効ユーザ」権限におけるファイルの読み取りが許可されていれば、ファイルを開くことができる。このプログラムでは、さらに、プロセスを実行中のユーザが、指定したファイルの所有者である場合に限り、そのファイルを開くことを意図している。ただし、このコードはファイルの識別にファイル名だけを使用している。
char *file_name; FILE *fd; /* file_name を初期化 */ fd = fopen(file_name, "r+"); if (fd == NULL) { /* エラー処理 */ } /* ファイルの読み取り */ fclose(fd); fd = NULL;
このコードが setuid-root プログラムの一部としてスーパーユーザ権限で実行されると、攻撃者がこのプログラムを悪用して、ユーザが所有していないファイルなど、実際のユーザ権限ではアクセスできないファイルを読み取ることができる。
適合コード (POSIX) (ファイルの所有者である場合)
以下の解決法では、open() 関数を使用してファイルを開いている。ファイルが正常に開いたら、fstat() 関数を使用してファイルに関する情報を stat 構造体に読み取る。この情報を、「実ユーザ」に関する既存の情報(getuid() および getgid() 関数で取得)と比較する。
struct stat st; char *file_name; /* file_name を初期化 */ int fd = open(file_name, O_RDONLY); if (fd == -1) { /* エラー処理 */ } if ((fstat(fd, &st) == -1) || (st.st_uid != getuid()) || (st.st_gid != getgid())) { /* ファイルがユーザのものでない */ } /*... ファイルからの読み取り ...*/ close(fd); fd = -1;
ファイル所有者のユーザ ID とグループ ID を、プロセスの実ユーザ ID と実グループ ID と照合することで、このプログラムは、プログラムの実ユーザが所有しているファイルだけにアクセスを限定することができる。この解決法を使用して、ファイルの所有者がプログラムが予期している所有者と同じであることを確かめ、たとえば攻撃者によって設定ファイルが悪意のあるファイルに差し替えられる機会を減らすことができる。
または、C99 の fopen() 関数を使用してファイルを開き、POSIX の fileno() 関数を使用して FILE ポインタをファイル記述子に変換することで、同じ解決法を実装することもできる。
リスク評価
ファイル関連の脆弱性の多くは、プログラムを意図しないファイルにアクセスさせるために悪用される。悪用を防ぐには、ファイルを正しく識別する必要がある。
レコメンデーション | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
FIO05-C | 中 | 中 | 中 | P8 | L2 |
自動検出
Compass/ROSE は、open() や fopen() の呼び出しの後に fstat() が呼び出されなかった場合を検出することによって、このレコメンデーションの違反の可能性を検出することができる。
参考情報
- [Drepper 06] Section 2.2.1 "Identification When Opening"
- [ISO/IEC 9899:1999] Section 7.19.3, "Files," and Section 7.19.4, "Operations on Files"
- [ISO/IEC PDTR 24772] "EWR Path Traversal"
- [MITRE 07] CWE ID 37, "Path Issue - Slash Absolute Path"; CWE ID 38, "Path Issue - Backslash Absolute Path"; CWE ID 39, "Path Issue - Drive Letter or Windows Volume"; CWE ID 62, "UNIX Hard Link"; CWE ID 64, "Windows Shortcut Following (.LNK)"; and CWE ID 65, "Windows Hard Link"
- [Open Group 04] "The open function," and "The fstat function"
- [Seacord 05] Chapter 7, "File I/O"
翻訳元
これは以下のページを翻訳したものです。