HOME情報セキュリティ資料・報告書・出版物調査・研究報告書情報セキュリティ技術動向調査(2008 年下期)

本文を印刷する

情報セキュリティ

情報セキュリティ技術動向調査(2008 年下期)

5 テンポラリファイルの扱い

田中 哲

背景

  2008年下期には、Perl の File::Path モジュールの rmtree 関数に関する CVE が 3件発表された。
(CVE-2008-2827, CVE-2008-5302, CVE-2008-5303)また、symlink attack に関する CVE は 100件以上出ている。
  テンポラリファイルの扱いに関する問題は古くからあるが、いまだに多くの問題が発生する。そこで本稿ではテンポラリファイルの扱いかたについて解説する。また、安全な削除に利用できる新しいシステムコールが提案されているので、それについても触れる。

1 テンポラリファイル

  テンポラリファイルはプログラムが一時的に利用するファイルである。
  Unix においては /tmp や /var/tmp というディレクトリが提供されており、すべてのユーザがそのディレクトリ下にテンポラリファイルを生成・削除するのが慣習である。本稿では、これらのディレクトリの代表として /tmp を例として用いる。
  この慣習では、/tmp を複数ユーザで共有する。そして、マルチユーザシステムにおいては他のユーザは必ずしも信用できない。そのため、テンポラリディレクトリ内でのファイル操作には他のユーザが任意のタイミングで介入する可能性があり、通常のファイル操作以上に注意が必要である。不適切に操作を行うと、意図しないファイルの削除や書き換えといったセキュリティ問題を引き起こすことがある。
  また、本稿では /tmp 直下にある他ユーザのファイルは unlink, rmdir, rename できないものと仮定する。これは sticky bit (S_ISVTX フラグ) の効果であるが、これについては後で述べる。
  さらに、テンポラリディレクトリはローカルなファイルシステムであることを仮定する。 つまり NFS 等のリモートファイルシステムについては考慮しない。

2 テンポラリファイルの作成

  ディレクトリでない、通常のファイルを /tmp 直下に生成する場合、open システムコールにおいて、第2引数 flags に O_EXCL フラグを指定しなければならない。O_EXCL は、ファイルが既に存在したら失敗するという指定である。また、第3引数 mode には 0600 など、他ユーザはアクセスできないパーミッションを指定する。
  たとえば、読み書き両用で foo という名前のテンポラリファイルの生成に挑戦する場合、以下のようになる 。

  int fd;
  fd = open("/tmp/foo", O_RDWR|O_CREAT|O_EXCL, 0600)
  if (fd == -1) {
    失敗に対応する
  }
	

  もし /tmp/foo が存在した場合、O_EXCL の効果によってこの open は失敗する。これにより、例えば他ユーザが /tmp/foo から重要なファイル(/etc/passwd や ~/.ssh/authorized_keys など) へのシンボリックリンクを作るという攻撃を行っても、重要なファイルを意図せず書き換えてしまうことを防ぐことができる
  また、パーミッションを 0600 とすることにより、umask の設定に依存せず、他ユーザがファイルを書き換えてしまうことを防げる。
  テンポラリファイルを生成するという目的のためには、ファイルが存在して失敗したときには、ファイル名を変えて再挑戦する必要がある。言語や環境によってはこの再挑戦まで含めて安全に行う機能がライブラリとして用意されており、可能ならそれらを使ったほうがよい。そのようなライブラリにはたとえば以下がある。

  •   C 言語の mkstemp (Single Unix Specification)
  •   C 言語の tmpfile (ISO C)
  •   シェルスクリプト用の mktemp コマンド (OpenBSD)
  •   Perl の File::Temp ライブラリ
  •   Python の tempfile ライブラリ
  •   Ruby の tempfile ライブラリ
  •   PHP の tmpfile

  なお、歴史的な経緯から、ファイル名を生成するだけの関数がライブラリに用意されていることがある。ファイル名を生成するだけの関数は、間違えた使いかたをしやすいため、基本的には避けるべきである。そのようなものにはたとえば以下がある。

  •   C 言語の mktemp (Single Unix Specification)
  •   C 言語の tempnam (Single Unix Specification)
  •   C 言語の tmpnam (ISO C)

3 テンポラリファイルの削除

  /tmp 直下にテンポラリファイルが安全に作成された場合、削除は単に unlink システムコールを使えばよい。
  たとえばテンポラリファイルの名前が /tmp/foo であれば、以下のように行う。

  if (unlink("/tmp/foo") == -1) {
    失敗に対応する
  }
	

4 テンポラリディレクトリの作成

  テンポラリディレクトリを安全に作成するには mkdir システムコールを使用する。その際、第2引数 mode には 0700 など、他ユーザはアクセスできないパーミッションを指定する。

  if (mkdir("/tmp/foo", 0700) == -1) { 
    失敗に対応する
  }

  open とは異なり、mkdir にはフラグ引数がない。mkdir は指定されたファイルが既に存在したときには常に失敗する。なお、指す先が存在しないシンボリックリンクが存在した場合も同様に失敗する。したがって、mkdir が成功すれば、テンポラリディレクトリの作成に成功したことが保証できる。
  また、パーミッションを 0700 とすることにより、umask の設定に依存せず、他ユーザがディレクトリを書き換えてしまうことを防げる。

5 テンポラリディレクトリの削除

  自分で作ったテンポラリディレクトリを削除する場合には、rmdir システムコールを使用すればよい。例えば以下のように行う。

  if (rmdir("/tmp/foo") == -1) { 
    失敗に対応する
  }
	

  ただし、rmdir は空のディレクトリしか削除できないため、rmdir に先立って内部のファイルをすべて削除しておかなければならない。サブディレクトリがあるのであれば、その内部のファイルを事前に削除しなければならず、一般には再帰的な処理が必要である。
  ここで以上の操作が安全なのは、信用できない他ユーザは書き込めないディレクトリを削除する場合である。そうではない場合、すなわち、信用できない他ユーザが書き込めるディレクトリを削除する場合には必ずしも安全ではない。これについて次に述べる。

6 他ユーザが書き込めるディレクトリの削除

  他ユーザが書き込めるディレクトリを削除するにはかなりの注意が必要である。冒頭に触れた Perl の rmtree の CVE もこの状況を扱う場合の話である。
  他ユーザが書き込めるディレクトリを扱う状況はたとえば以下が考えられる。

  •   テンポラリディレクトリを作成するときに、mkdir に指定するパーミッションとして
      たとえば 0777 を指定してしまうと、umask によっては誰でも書き込めるディレ
      クトリができてしまう。
  •   複数ユーザでディレクトリを共有してファイルを管理する状況。なお、この想定は
      テンポラリでないファイルを扱う状況である。
  •   一般ユーザのテンポラリディレクトリを管理者が削除する場合、管理者にとって、
      その一般ユーザという他ユーザが書き込めるディレクトリを扱うことになる。(な
      お、一般ユーザがディレクトリを削除するとき、そのディレクトリは管理者という
      他ユーザが書き込めるが、 管理者を信用するのは前提であるため、これは危険
      とはみなされない。)

  最初の状況を避けるのは簡単である。テンポラリディレクトリを生成するときに、mkdir に指定するパーミッションで 0700 を指定すればよい。
  しかし、後のふたつの状況を避けることは困難である。複数ユーザで意図的にディレクトリを共有するのであれば、ときに削除が必要になる。また、管理者が一般ユーザのテンポラリディレクトリを削除するのは、/tmp を掃除するときに必要になる。
  信用できない他ユーザが書き込めるディレクトリを削除するのが危険である理由を例によって説明する。
  ユーザ A が /tmp/foo に以下のようなディレクトリおよびファイルを作って放置し、管理者が削除することを決定したと仮定する。

  /tmp/foo        所有者がユーザ A のディレクトリ
  /tmp/foo/passwd  所有者がユーザ A のファイル
  rmdir  システムコールでディレクトリを削除する場合、中身が空でなければならない。

  このためディレクトリを削除するツールは、削除対象のディレクトリから末端のファイルまでたどり、末端のファイルから削除を始める。
  ディレクトリ構造をたどるには、ディレクトリとそれ以外のファイルを区別し、ディレクトリの場合は再帰的に削除を行うという判断が必要である。このとき、あるパス名が指す実体がディレクトリかどうかを判断するには lstat システムコールを使う。lstat はシンボリックリンクをたどらないため、ディレクトリと、ディレクトリを指すシンボリックリンクを区別できる。
  また、ディレクトリであると判明した後に、その直下に存在するファイル名を得るには opendir/readdir/closedir 関数を用いる。以下ではこれらを opendir とだけ記述する。 これを例に適用すると素朴には次の動作となる。

  1. /tmp/foo がディレクトリかどうか lstat で調べる -> ディレクトリである
  2. /tmp/foo ディレクトリの中身を opendir で調べる -> passwd がある
  3. /tmp/foo/passwd がディレクトリであるかどうか lstat で調べる -> ディレクトリでない
  4. /tmp/foo/passwd を unlink で削除する
  5. /tmp/foo を rmdir で削除する

  なお、ここで例にあげたディレクトリ構造ではディレクトリにひとつのファイルしか入っていないため、上記のようなシーケンシャルな動作になるが、一般にはひとつのディレクトリに複数のファイル・ディレクトリが入っているため、再帰的な動作が必要になる。
  ところで、/tmp/foo はユーザ A が所有しているため、ユーザ A はいつでも書き換えられる。ここで、4 の直前、すなわち管理者が /tmp/foo/passwd を lstat でディレクトリでないと判断した後、/tmp/foo/passwd を unlink で削除する前に、以下の変更を加えた場合を考える。

  3.1. ユーザ A が /tmp/foo を /tmp/bar に rename システムコールで改名する
  3.2. ユーザ A が /tmp/foo から /etc へのシンボリックリンクを symlink システムコールで作成する

  この場合、4 で管理者は /tmp/foo/passwd を削除するが、これは /tmp/foo が /etc へのシンボリックリンクになった後であるため、/etc/passwd を削除することになる。 /etc/passwd は /tmp/foo 内のファイルでは無く、これは意図された削除ではない。/etc/passwd が削除されると、ログインできなくなる等、多くの問題が発生する可能性がある。つまり、ユーザ A によるシステムへの攻撃が成功する 。
  このように上述の素朴なディレクトリ削除は危険である。
  同様に、複数ユーザでディレクトリを共有した場合、共有ディレクトリ内で素朴な削除を行った場合、共有していないユーザ個人のファイルの削除を引き起こす可能性がある。
  この危険性は、/tmp/foo/passwd というパス名が指すファイルの実体が/, /tmp, /tmp/foo というパス内の上位の要素の変化に伴って変わりうる、という点が原因である。/ と /tmp が指すファイルは管理者しか変更できないので問題ないが、/tmp/foo はユーザ A が変更できるので現実的な問題となる。
  上記の素朴な削除では、/tmp/foo がシンボリックリンクでない本物のディレクトリであることを一度は確認しており、その確認した時点では /tmp/foo/passwd を削除すべきであるという判断は正しい。しかし、/tmp/foo はユーザ A がいつでも変更できるので、実際に削除する時点でもその判断が正しいという保証はなく、そのずれが脆弱性を生んでいる。このように確認してから実際に行うまでの時間差の問題を TOCTTOU (Time of Check to Time of Use) 問題という。
  これを安全に削除するひとつの方法は、chdir システムコールを用いてカレントディレクトリを移動し、unlink に与えるパスは / を含まない単純なファイル名にすることである。chdir でカレントディレクトリを移動した後、本当に削除対象のファイルがあるディレクトリに移動できたかどうかを検査し、意図どおりに移動できていなかったら中断する。 chdir はファイルシステムを変更しない操作なので、ここで中断すればファイルシステムに悪影響を与えない。そして、/ を含まない単純なファイル名での unlink は、カレントディレクトリ直下のファイルしか削除できない。このため、unlink によってどのファイルが削除されても、それは削除対象ディレクトリ直下のファイルであるから削除対象であり、削除して問題ない 。
  ここでカレントディレクトリが本当に意図したディレクトリであるかどうかという検査が必要になる。これは lstat システムコールで得た st_dev (デバイス番号) と st_ino (iノード番号) を比較することで行う。削除対象のディレクトリのパス名で lstat した結果と、chdir した後にカレントディレクトリ "." を lstat した結果で st_dev と st_ino がそれぞれ一致すれば、意図どおりのディレクトリに移動できたことがわかる。もし異なれば、lstat してから chdir するまでにディレクトリがシンボリックリンクにすり替えられる等の攻撃を受けていることになる。
  この方法により前述の /tmp/foo を安全に削除すると以下のようになる。

  1. /tmp を lstat して結果を記録する
  2. /tmp に chdir で移動する
  3. カレントディレクトリを lstat して /tmp の lstat の結果と比較する -> 異なっていたら中断する
  4. foo を lstat してディレクトリかどうか調べる -> ディレクトリである
  5. foo に chdir で移動する
  6. カレントディレクトリを lstat して、foo の lstat の結果と比較する -> 異なっていたら中断する
  7. カレントディレクトリの中身を opendir で調べる -> passwd がある
  8. passwd を lstat でディレクトリかどうか調べる -> ディレクトリでない
  9. passwd を unlink で削除する
  10. 親ディレクトリ ".." に chdir する
  11. カレントディレクトリを lstat して、/tmp の lstat の結果と比較する -> 異なっていたら中断する
  12. foo を rmdir で削除する

  このように削除しているとき、ユーザ A が /tmp/foo を /tmp/bar に rename し、/tmp/foo を /etc へのシンボリックリンクにする攻撃を行うと考える。
  この攻撃が 4 以前に行われると、foo はディレクトリでないと判断され、攻撃で作られたシンボリックリンク foo を unlink して終了する。つまり、/etc/passwd は unlink されず、攻撃は失敗する。
  攻撃が 4 と 5 の間で行われると、5 の chdir では /etc にカレントディレクトリが移動することになる。しかし、6 の検査で意図しないディレクトリに移動したことが検出されて中断する。つまり、/etc/passwd は unlink されず、攻撃は失敗する。
  攻撃が 5 以降 12 以前に行われると、削除開始時点において /tmp/foo/passwd に存在したファイルの削除が行われる。/etc/passwd は unlink されず、攻撃は失敗する。なお攻撃が 9以前である場合は、削除されるファイルは /tmp/bar/passwd という削除対象外のパスに移動している。しかし、削除開始時点においては /tmp/foo/passwd という削除対象であったため、これを削除することは間違いでない。
  攻撃が 12以降に行われた場合、/tmp/foo が存在しないため、/tmp/foo を /tmp/bar に rename することができない。このため、攻撃は失敗する。
  このように、どのタイミングで攻撃が起きても、/etc/passwd は unlink されず、攻撃は失敗する。
  一般には、chdir でをカレントディレクトリに移動したときに、意図どおりに移動できたことを確認すれば、関係ないファイルの削除を防ぐことができる。

7 新システムコール: openat とその仲間

  前節では他ユーザが書き込めるディレクトリを chdir を用いて安全に削除する方法について述べた。
  しかし、chdir はカレントディレクトリというプロセス属性を変更するため、スレッドセーフでない。これは rm のような削除専用コマンドであれば問題にならないが、Perl などのように用途を限定しない言語のライブラリとしては問題がある。
  スレッドセーフかつ安全に他ユーザが書き込めるディレクトリを削除するのは簡単でない。
  カレントディレクトリに依存せずに /tmp/foo/passwd を削除するには、/tmp/foo/passwd というパス名を unlink に与えざるを得ない。したがって、他ユーザが /tmp/foo を変更できるとすれば危険性を排除できない。この危険性を排除するためには、/tmp/foo を他ユーザが変更できないようにすることが考えられる。たとえば、/tmp/foo の所有者を自分に変更して、パーミッションを 700 に変えるなどである。
  しかし、所有者を変更することは管理者しかできない。また、所有者やパーミッションの変更はそれ自体が symlink attack の対象になるため、注意深い実装が必要である。
  スレッドセーフかつ安全に他ユーザが書き込めるディレクトリを削除することにはさまざまな困難があり、可能な限り避けたほうがよい。とくに、テンポラリディレクトリを生成・削除するだけであれば、生成時にパーミッションを 0700 に設定するだけで避けられるのでぜひそうやって避けるべきである。
  ただし、最近、安全なディレクトリの削除にも利用できる新しい API が実装・提案されている。openat に代表されるこの API は、ファイルディスクリプタを活用して、パス名をたどってファイルの実体を得る操作を削減可能とする。まず、パス名を受け取る多くのシステムコールに対応して、加えてカレントディレクトリの代わりになるファイルディスクリプタも受け取るシステムコールが用意される。また、パス名を受け取る代わりにファイルディスクリプタを受け取るシステムコール・関数もいくつか用意される。
  たとえば、open システムコールに対応して、openat システムコールが定義される。openat は open の引数に加え、ディレクトリを示すファイルディスクリプタを受け取り、パス名が相対パスだったときに、起点にカレントディレクトリでなく指定されたファイルディスクリプタを用いる。
  また、これらのシステムコールの多くにはフラグ引数も追加され、動作を修飾できる。たとえば、指定したファイルディスクリプタでなく、プロセスのカレントディレクトリを用いるというフラグが指定ができる。また、fstatat システムコールでは、シンボリックリンクをたどるかどうかを指定でき、stat と lstat のどちらの動作も選べる。unlinkat システムコールはディレクトリの削除かどうかを指定でき、unlink と rmdir のどちらの動作も選べる。
  さらに fdopendir 関数が用意され、open して得たファイルディスクリプタをもとに DIR 構造体を生成できる。これにより、ディレクトリを open した結果を、readdir/closedir に使うことができる。
  これらにより、カレントディレクトリに依存せずに安全なディレクトリの削除を実装できる。つまり、カレントディレクトリの代わりにディレクトリを open したファイルディスクリプタを使えばよい。たとえば、前述した /tmp/foo/passwd の削除は以下のように実現できる。

  1. /tmp を lstat して結果を記録する
  2. /tmp を open する -> fd_tmp が得られる
  3. fd_tmp を fstat して /tmp の lstat の結果と比較する -> 異なっていたら中断する
  4. fd_tmp から相対で foo を fstatat してディレクトリかどうか調べる -> ディレクトリである
  5. fd_tmp から相対で foo を openat する -> fd_foo が得られる
  6. fd_foo を fstat して foo の fstatat の結果と比較する -> 異なっていたら中断する
  7. fd_foo の中身を fdopendir で調べる -> passwd がある
  8. fd_foo から相対で passwd を fstatat でディレクトリかどうか調べる -> ディレクトリでない
  9. fd_foo から相対で passwd を unlinkat で削除する
  10. fd_foo を close する
  11. fd_tmp から相対で foo ディレクトリを unlinkat で削除する
  12. fd_tmp を close する

  なお、ここで fstatat はシンボリックリンクをたどらないようにフラグを指定して使用するものとする。
  この方法は、chdir による方法と同様に安全であり、かつ、カレントディレクトリに依存していないのでスレッドセーフである。
  openat は Solaris 9 で実装され、Linux 2.6.16 でも実装された。POSIX の次期バージョンにも提案されており、広く普及していくことが期待される。

8 sticky bit

  本稿では /tmp 直下にある他ユーザのファイルは unlink, rmdir, rename できないものと仮定している。これは /tmp には sticky bit (S_ISVTX フラグ) が設定されていることを意味している。
  Unix では原則的に、あるファイルの unlink や rename はそのファイル自身の所有者には関係なく、そのファイルが存在するディレクトリに書き込み権限があるかどうかによって決まる。
  この原則を /tmp にそのまま適用すると、/tmp は誰でも書き込み可能であるから、 直下に作ったテンポラリファイルは誰のファイルでも削除・改名できることになる 。
  この場合、自分が作ったテンポラリファイルを後で open して使ってはならない。たとえば他のコマンドにパス名を渡して使わせるのも危険である。そのコマンドがそのパス名で open するとき、そこにあったファイルは削除されたり、別のファイルにすり替えられている可能性がある。
  このように自由に削除・改名できてしまうのは不適切であるため、ディレクトリには sticky bit を設定できる。
  sticky bit が設定されているディレクトリ下のファイルは、その sticky bit の設定されているディレクトリもしくは削除対象のファイルの所有者か、あるいは特権があるユーザしか unlink はできない。unlink だけでなく、rename にも同様な制約がある。
  この制約により、/tmp 直下にテンポラリファイルを生成すると、生成したファイルは信用できない他ユーザにすり替えられることはない。

9 環境変数 TMPDIR

  ここまでに述べたように、テンポラリファイルを安全に扱うには注意が必要である。しかし、そもそもそのように注意が必要なのは、ユーザ全員が /tmp を共有しているという理由が大きい。
  もし、/tmp のような共有ディレクトリを用いず、個々のユーザ専用のディレクトリ内にテンポラリファイルを生成するのであれば、危険性はかなり抑制される。
  そこで、テンポラリファイルを生成するアプリケーションは、生成するディレクトリを外部から指定できることが望まれる。
  そして、外部からテンポラリファイルのためのディレクトリを指定するには環境変数 TMPDIR を使うという慣習がある。もし TMPDIR が設定されているなら、そのディレクトリ内にテンポラリファイルを生成することが期待される。
  アプリケーションが TMPDIR をサポートしていれば、/tmp を避けることが可能となる。
  ただし、TMPDIR を使うには TMPDIR 環境変数が悪意をもって設定されてはならない。従って、リモートから環境変数を任意に設定できる機能を持っているアプリケーションでは素朴にサポートしてはならない。
  なお、C 言語の mkstemp, tmpfile 関数は TMPDIR をサポートしていないので、サポートするには別途コードが必要になる。また、OpenBSD の mktemp コマンドは -t オプションにより TMPDIR を使うようになる。

10 シェルスクリプト

  シェルスクリプトでテンポラリファイルを安全に生成することは簡単でないので、ここで触れる。
  シェルスクリプトでテンポラリファイルを作る伝統的な方法は /tmp/foo.$$ などといったファイル名を使うことである。$$ はプロセス番号に展開されるため、他のプロセスとファイル名は衝突しないという意図である。以下に例をあげる。これは日付を (無駄にテンポラリファイルを経由して) 表示するスクリプトであるが、とくに意味はない。

  date > /tmp/foo.$$
  cat /tmp/foo.$$
  rm /tmp/foo.$$

  しかし、このテンポラリファイルの生成は危険である。
  衝突しないというのは、他のユーザも協調してテンポラリファイルの名前にプロセス番号を使うときの話であり、悪意あるユーザにとって、そうでない番号をファイル名に使うことを防げる機構は存在しない。
  たとえば、ユーザ baz が /tmp/foo.1000 に /home/foo/.ssh/authorized_keys を指すシンボリックリンクを作った場合、ユーザ foo が上記のように /tmp/foo.$$ というテンポラリファイルを生成し、偶然プロセス番号が 1000 だった場合、/home/foo/.ssh/authorized_keys が破壊されてしまう。
  これを避けるためには先に述べたとおり、テンポラリファイルを生成するときに O_EXCL を指定すればよい。
  これを POSIX シェルで行うには、set -C を使う。set -C は noclobber オプションを有効にする。noclobber が有効な場合、> によるリダイレクトでの open には O_EXCL が指定される。
  これを使ってテンポラリファイルの生成に挑戦し、失敗したときは終了するには以下のようにする。また、ここでは TMPDIR が指定された場合には使うようにしている。

  t="${TMPDIR:-/tmp}/foo.$$"
  set -C
  : > "$t" || exit 1
  set +C
  date > "$t"
  cat "$t"
  rm "$t"

  失敗したときにファイル名を変えて再挑戦するのであれば以下のようにする。

  set -C
  n=1
  t="${TMPDIR:-/tmp}/foo.$RANDOM$n$$"
  while ! : > "$t"; do
   n=`expr $n + 1`
   t="${TMPDIR:-/tmp}/foo.$RANDOM$n$$"
  done
  set +C
  date > "$t"
  cat "$t"
  rm "$t"

  なお、ここで $RANDOM と $$ を使っているが、これらは攻撃に対する防御として本質的なものではない。他ユーザの意図的な攻撃からの防御はこれまでに述べたとおり set -C による O_EXCL が本質である。ただし、$RANDOM や $$ には意図的でない衝突の確率を減らす効果がある。
  また、標準化されているものではないが、システムが mktemp コマンドを提供していればこれを使うこともできる。mktemp は引数に指定したテンプレートに従って安全にテンポラリファイルを生成し、生成したファイルのパスを出力する。この mktemp コマンドは OpenBSD が発祥である。また、GNU coreutils 6.10 でも提供されている。

  t=`mktemp -t foo.XXXXXXXXXX`
  date > "$t"
  cat "$t"
  rm "$t"

11 まとめ

  テンポラリファイルの扱いとシンボリックリンク攻撃へ対処について述べた。この問題は古くから知られているが、まだ多くの問題が発生している。また、対処法についても openat など新しい技術が開発されている。

以上

12 参考文献