第9章 ファイル対策
ファイルレースコンディション対策

ファイルレースコンディション

レースコンディションは、ある状態が成立しているという前提でプログラムが処理を行ったところ、実際には他のプロセスやスレッドによって状態が変更されていて、意図した処理が失敗してしまう問題のことである。

ファイルレースコンディションは、プログラムがファイルを取り扱う際に起こるレースコンディションである。Unix系およびGNU/Linux系のプラットフォームで標準的なファイル入出力APIを用いているプログラムで起こり得る。

ファイルのすり替え

ファイルレースコンディションの典型的な例として、存在の有無や属性等の状況を確認した上でファイルをオープンするという場面で、確認とオープンの間の僅かな時間に対象がシンボリックリンクにすり替えられるというものがある。別の実体を指すシンボリックリンクに誘導されて想定外のファイルアクセスが起こり、被害が生じる。

これは次のようにして起こる。プログラムのアクセス先をシンボリックリンクを設けることですり替える攻撃手口がある。この攻撃を避けるため、プログラムの側は lstat() 関数を用いてパス名が指す実体がシンボリックリンクでないか調べることが考えられる。ファイルが存在することと、それがシンボリックリンクでないことを確認できたら open() 関数でファイルをオープンするようにするが、次のようなコードではファイルレースコンディションの発生を防げない。

 1    int fd, err;
 2    struct stat lstat_result;
 3
 4    if (lstat(pathname, &lstat_result) != 0) return ERROR;
 5       // lstat()失敗なら、エラー処理
 6    if ((lstat_result.st_mode & S_IFMT) == S_IFLNK) return ERROR;
 7       // pathnameがシンボリックリンクなら、エラー処理
 8
 9    // ← 注意:この時点で「すり替え」が起こっている可能性がある
10
11    fd = open(pathname, O_RDONLY, 0);
12       // ファイルをオープン
13    if (fd < 0) return ERROR;
14       // open()失敗なら、エラー処理
15
16    err = read(fd, buffer, size);
17       // ファイル内容へのアクセス
18    ...

lstat() 関数の呼び出しの後、open() 関数を呼び出す前のタイミングを狙って攻撃者がシンボリックリンクを設置することに成功すると、プログラム側はすり替えに気づかず処理を進めてしまう。

ファイルレースコンディション対策
図9-3: ファイルレースコンディション対策

推奨するファイル入出力処理の流れ

ファイルレースコンディションの発生を回避しつつファイルにアクセスするためには、lstat() でファイルの属性を検査するのみならず、次のようにしてファイルのすり替えを検知するようにする。

  1. lstat() を用いて対象ファイルのデバイス番号とiノード番号を取得しておく
  2. open() でファイルをオープンする。ファイル記述子が得られる
  3. そのファイル記述子を用いた fstat() 呼び出しによって、ふたたびデバイス番号とiノード番号を取得する
  4. 1と3の間でデバイス番号とiノード番号が変化していなければファイルのすり替えは起こっていないと判定する

例えば、次のようなコードになる。

コード例 ファイルレースコンディションを避けて既存ファイルをオープンする関数 open_safely()
 1 int open_safely(const char* pathname) {
 2    int fd;
 3    struct stat lstat_result, fstat_result;
 4 
 5    if (lstat(pathname, &lstat_result) != 0) return -1; 
 6       // パス名が示す対象の属性を得る。
 7       // シンボリックリンクであるか否かも含めた属性が得られる。
 8       // lstatに失敗したら、エラー終了。
 9
10    if (!S_ISREG(lstat_result.st_mode)) return -1;
11       // シンボリックリンクであることを含め、
12       // パス名が指す対象が通常のファイル以外ならエラー終了。
13 
14    fd = open(pathname, O_RDWR, 0);
15       // ファイルをオープンしてファイル記述子fdを得る。
16    if (fd < 0) return -1;
17       // openに失敗したら,エラー終了。
18 
19    if (fstat(fd, &fstat_result) != 0) {
20       // ファイル記述子fdが示す対象の属性を得る。
21       // fstatに失敗したら、fdをクローズしてエラー終了。
22       close(fd);
23       return -1;
24    }
25
26    if (lstat_result.st_ino != fstat_result.st_ino ||
27        lstat_result.st_dev != fstat_result.st_dev) {
28       // lstat()結果とfstat()結果が指す対象は同一か調べる。
29       // iノード番号とデバイス番号を照合する。
30       // 一致しなければ、fdをクローズしてエラー終了。
31       close(fd);
32       return -1;
33    }
34 
35    return fd;
36       // fdを返して正常終了。
37 }

ファイルへのアクセスに fopen 系の関数を使う場合にも、次のようにすることにより、この方法を用いることができる。

 1 FILE* fopen_safely(const char* pathname) {
 2    FILE* s;
...
14    s = fopen(pathname, "r");
15       // ファイルをオープンしてストリーム s を得る
16    if (s == NULL) return NULL;
17       // fopenに失敗したら、エラー終了
18
19    if (fstat(fileno(s), &fstat_result) != 0) {
...

パス名を引数にとるAPIへの対策

ファイル入出力関連のAPI関数はしばしば引数にパス名をとるが、ファイルレースコンディション問題があるためパス名が指す対象の同一性は必ずしも保証されない。パス名を引数にとるAPI関数の使用には注意が必要である。

(1) 使用を控えた方が良い関数

次の関数は使用を控え、他の代替関数を用いる。

stat
statを用いず、lstat→open→fstat の組み合わせを用いる。
chmod
chmodを用いず、lstat→open→fstatで検証したのち、fchmodを用いる。
chown
chownを用いず、lstat→open→fstatで検証したのち、fchownを用いる。
utime, utimes
utimeおよびutimesを用いず、lstat→open→fstatで検証したのち、futimesを用いる。
creat
creatを用いず、open(path, O_CREAT | O_EXCL | O_WRONLY, mode) を用いる。

(2) 組み合わせによる対策

次の関数は単独で用いず、fstat等と組み合わせる。

lstat
lstatを単独で用いず、lstat→open→fstatの組み合わせを用いる。
open
openを単独で用いず、lstat→open→fstatの組み合わせを用いる。

(3) ファイル記述子による代替手段のない関数

次の関数にはファイル記述子による代替手段がない。アクセスパーミッション設定を厳しくする等で対処する必要がある。

link
リンクを設ける対象をファイル記述子で指定する方法はなく、パス名で参照せざるを得ない。アクセス許可を厳しく設定することにより対象のファイルを保護する。
execve, execl, execlp, execle, execv, execvp, execvP
実行するプログラムをファイル記述子で指定する方法はなく、パス名で参照せざるを得ない。実行するプログラムは、十分に保護されたディレクトリに格納し、そのディレクトリに対しては限られたアカウントにのみアクセスを許す。
tmpfile, tmpnam, tempnam
一時ファイル名または一時ファイルそのものを作る関数。

(4) 参考:シンボリックリンクの影響を受けない関数

次の関数は、処理系によって実装が異なる可能性もあるが、多くの場合シンボリックリンクの影響を受けない。

unlink, mkdir, mkfifo, symlink

まとめ

ファイルレースコンディションの問題があるため、ある時点でパス名が指していた対象と、次の瞬間、その同じパス名が指している対象が同一である保証はない。パス名を引数にとる入出力系のAPI関数を使う際は、対象が同一であることを確認するロジックを付け加える等の対策が必要である。