テンポラリ・ファイルを作成する時に、アトミックな操作を正しく実行する上での
問題が顕著に現れます。
これまで Unix ライクなシステムでは、テンポラリ・ファイルは /tmp もしくは
/var/tmp ディレクトリに作ってきており、ユーザすべてが共有していました。
安全が必要なプログラムが動作している間、他のファイル(たとえば、/etc/passwd)
に対するシンボリックリンクをテンポラリ・ディレクトリに作成する罠を攻撃者は
仕掛けてきました。
攻撃者の狙いは、安全が必要なプログラムが、ある特定のファイル名が存在しないと
判断する状況を作り上げてから、攻撃者が別のファイルへのシンボリックリンクを
張って、安全が必要なプログラムにある操作を実行させる状態です(実際は意図して
いないファイルを開いてしまっている)。
この方法でよく重要なファイルが壊されたり、修正されたりします。
普通のファイルを作成するようなこの手の攻撃のバリエーションはたくさんあります。
この攻撃は、安全なプログラムで使用するテンポラリ・ファイルが存在するのと同じ
ディレクトリに、攻撃者がファイルシステム・オブジェクトを作成できる
(さもなければアクセスできる)という仮定にもとづいています。
共有ディレクトリにファイルを作成する上で共通の問題点は、使用を予定している
ファイル名が、作成時に既に存在していないことを保証しなければいけない点です。
ファイルを作成する「前」にチェックするのは効き目がありません。理由は、
チェック後かつファイルの作成前に、別のプロセスがそのファイル名でファイルを
作成できてしまうからです。
「予測不可能」もしくは「ユニーク」なファイル名を使うのも、およそ効果があり
ません。
それは、名前の推測が成功するまで、別プロセスが何度でも推測できるからです。
基本的に、共有している(sticky をかけてある)ディレクトリでテンポラリ・
ファイルを作成するには、次のことを繰り返し行う必要があります。(1)「ランダム」
なファイル名を作成すること、(2)O_CREAT | O_EXCL を使って open し、
パーミッションできつい制限をかけること、(3)open が成功したなら、繰り返さない
こと、です。
1997 年版の「Single Unix Specification」によると、任意にテンポラリ・ファイル
を作成するのに望ましい方法は、tmpfile(3)を使う、となっています。
tmpfile(3)関数はテンポラリ・ファイルを作成し、それに対応したストリームを
open し、そのストリームのディスクリプタを返します(失敗すると NULL を返します)。
あいにく、ファイルが安全に作成される保証は仕様上一切ありません。
このドキュメントの旧版で、実装すべてが安全かどうか確信できないので心配だ、
と述べました。
その後、古い System V システムで tmpfile(3)の実装が安全ではないことがわかって
います(tmpnam(3) と tempnam(3)も同様に安全でない)。
もちろん tmpfile(3)を実装しているライブラリは、そのようなファイルを安全に
作成すべきですが、ユーザはシステムのライブラリにセキュリティ上の欠陥がある
ことを、必ずしも気付くわけではありません。場合によってはその件について、
何も打つ手がない時もあります。
Kris Kennaway 氏は、テンポラリ・ファイルの作成に当たって、一般に mkstemp(3)
の使用を推奨しています。
テンポラリ・ファイルを作るなら、自分自身で関数を作り上げて利用するよりも、
よく知らたライブラリを使った方が良い、というのが理屈です。そしてこの関数は
よく知られた使い方を採用しています。
これはかなりもっともな見解です。
mkstemp(3)を使うなら、私はこれに加えて必ず umask(2)を使って、テンポラリ・
ファイルのパーミッションが所有者だけになるように制限をかけます。
これは mkstemp(3)の実装(基本的には古い物)には、テンポラリ・ファイルを
すべてのユーザに対して、読み書き可能にしているものがあるからです。この状態
になると攻撃者は、このディレクトリにプライベートなデータを読み書き可能に
なります。
多少厄介なのは、mkstemp(3)が直接には TMP や TMPDIR といった環境変数をサポート
していない点です(下記で論じます)。そこで環境変数をサポートしたいとなると、
自分でサポートできるようにコードを追加しなければいけません。
ここで、環境変数をサポートした C で書いた mkstemp(3)の使い方を示したプログラム
を掲載します。これで、TMP もしくは TMPDIR のサポートを追加することで、直接
両者の操作が可能になります。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
void failure(msg) {
fprintf(stderr, "%s\n", msg);
exit(1);
}
/*
* Given a "pattern" for a temporary filename
* (starting with the directory location and ending in XXXXXX),
* create the file and return it.
* This routines unlinks the file, so normally it won't appear in
* a directory listing.
* The pattern will be changed to show the final filename.
*/
FILE *create_tempfile(char *temp_filename_pattern)
{
int temp_fd;
mode_t old_mode;
FILE *temp_file;
old_mode = umask(077); /* Create file with restrictive permissions */
temp_fd = mkstemp(temp_filename_pattern);
(void) umask(old_mode);
if (temp_fd == -1) {
failure("Couldn't open temporary file");
}
if (!(temp_file = fdopen(temp_fd, "w+b"))) {
failure("Couldn't create temporary file's file descriptor");
}
if (unlink(temp_filename_pattern) == -1) {
failure("Couldn't unlink temporary file");
}
return temp_file;
}
/*
* Given a "tag" (a relative filename ending in XXXXXX),
* create a temporary file using the tag. The file will be created
* in the directory specified in the environment variables
* TMPDIR or TMP, if defined and we aren't setuid/setgid, otherwise
* it will be created in /tmp. Note that root (and su'd to root)
* _will_ use TMPDIR or TMP, if defined.
*
*/
FILE *smart_create_tempfile(char *tag)
{
char *tmpdir = NULL;
char *pattern;
FILE *result;
if ((getuid()==geteuid()) && (getgid()==getegid())) {
if (! ((tmpdir=getenv("TMPDIR")))) {
tmpdir=getenv("TMP");
}
}
if (!tmpdir) {tmpdir = "/tmp";}
pattern = malloc(strlen(tmpdir)+strlen(tag)+2);
if (!pattern) {
failure("Could not malloc tempfile pattern");
}
strcpy(pattern, tmpdir);
strcat(pattern, "/");
strcat(pattern, tag);
result = create_tempfile(pattern);
free(pattern);
return result;
}
main() {
int c;
FILE *demo_temp_file1;
FILE *demo_temp_file2;
char demo_temp_filename1[] = "/tmp/demoXXXXXX";
char demo_temp_filename2[] = "second-demoXXXXXX";
demo_temp_file1 = create_tempfile(demo_temp_filename1);
demo_temp_file2 = smart_create_tempfile(demo_temp_filename2);
fprintf(demo_temp_file2, "This is a test.\n");
printf("Printing temporary file contents:\n");
rewind(demo_temp_file2);
while ( (c=fgetc(demo_temp_file2)) != EOF) {
putchar(c);
}
putchar('\n');
printf("Exiting; you'll notice that there are no temporary files on exit.\n");
} |
Kennaway 氏は、mkstemp(3)を使えないなら、mkdtemp(3)を使ってディレクトリを
つくるように推奨しています。こうすれば、外部から守れます。
最終的に、安全でない mktemp(3)を使わなければならないなら、予測できない文字を
たくさん使うようにも提案しています。 10 文字がお勧めです(libc が許せば)。
こうすれば、ファイル名は簡単には推測できなくなります(6 文字だと、5 文字は PID
で取られてしまうので、ランダムに残されたのは 1 文字だけになってしまいます。
これでは攻撃者に簡単に競合状態をしかけられてしまいます)。
これに加えて tmpnam(3)の利用も避けるように提案します。スレッドが動いていて
tmpnam(3)を使用をすると、どうなるのかわかりません。また TMP_MAX を越えて使用
すると(実用上、1 つのループ内で使用しなければいけません)正しい動作が保証
できません。
概して mktemp(3) や tmpnam(3)のような、安全でない関数の使用は避けるべきです。
使用するなら、セキュリティを脅かす点に特別な処置を講じたり、安全なライブラリ
の実装のテストをインストールの一環として行ってください。
問題がいろいろあってもなお、/tmp や誰でも書ける(もしくはグループを信頼して
いないなら、グループで書ける)ディレクトリにファイルを作って、mk*temp()を使い
たくないなら(たとえば、名前が事前にわかっているファイルを意図して)、
常に O_CREAT と O_EXCL フラグを付けて open()を呼び出し、
返り値をチェックしてください。
open()が失敗したなら、その時は適切に後処理してください(たとえば、exit する)。
GNOME のプログラミング・ガイドラインでは、ファイルシステム・オブジェクトを
共有の(テンポラリの)ディレクトリに作成する場合、下記の C コードを推奨
しています。これは最小限のセキュリティでファイルを作成するのが目的です。
char *filename;
int fd;
do {
filename = tempnam (NULL, "foo");
fd = open (filename, O_CREAT | O_EXCL | O_TRUNC | O_RDWR, 0600);
free (filename);
} while (fd == -1); |
ここで注目なのは、安全でない関数の tempnam(3)が使われていても、ループ内部
で O_CREAT と O_EXCL を使って、セキュリティ上の弱点をカバーしている点です。
注目して欲しいのは、ファイル名を free()する必要があるところです。
処理が終わったら、close()や unlink()すべきです。
標準 C 入出力ライブラリを使いたいなら、fdopen()を「w+b」で使用して、ファイル
ディスクリプタを FILE * に変えてください。
この解決方法は、NFS version 2 (v2)のシステムではうまくいかないでしょう。理由
は、古い NFS が O_EXCL をきちんとサポートしていないからです。
この解決方法にはちょっとした欠点もあって、tempnam を安全に使わないと、
コンパイラやセキュリティ・スキャナがうるさく警告を出すかもしれません。
これは mkstemp(3)では問題となりません。
シェルスクリプトでテンポラリ・ファイルが必要ならば、パイプを使ってローカル
ディレクトリ(たとえば、ユーザのホームディレクトリ内のどれかに)や、場合に
よってはカレントディレクトリを利用するのが適切でしょう。
こうすれば、ユーザが許可しない限りは共有はありえません。
どうしても /tmp のような共有ディレクトリにテンポラリファイルを作りたい、
もしくは必要なら、従来からのシェル上のテクニックを使って、ファイル名のひな形
にプロセス id を組み込み、いつも通りに「>」でファイルを作っては、いけません。
シェルスクリプトは「$$」を使って pid を示しますが、攻撃者は簡単に pid を
特定もしくは推測できます。そうして攻撃者は、同じ名前で事前にファイルを作成
したり、リンクしたりしてしまいます。
つまり、下記の「ありがち」なシェルスクリプトは、安全ではありません。
echo "This is a test" > /tmp/test$$ # DON'T DO THIS. |
シェルスクリプトでテンポラリファイルが必要でかつ、/tmp に置きたい場合は、
mktemp(1)が解決方法になるのと思います。mktemp(1)はシェルスクリプトでの利用
を前提にしています。
mktemp(1)と mktemp(3)は別物で、mktemp(1)は安全です。
正直言うと、私はシェルスクリプトで共有ディレクトリにテンポラリファイルを
しょっちゅう作っているわけではありません。
そのようなファイルをプライベートなディレクトリに作成するか、パイプを使うか
する方が好ましいと思います。
しかしどうしても必要なら、mktemp(1)でひな形を作って、O_EXCL でファイルや
ディレクトリを作成し、最終的にファイル名を返すようにします。
O_EXCL を使えば、/tmp のような共有ディレクトリでも安全になります(ただし NFS
version 2 を使っていなければ)。
ここで、正しい例として mktemp(1) を Bourne シェルで利用してみます。
この例は mktemp(1)の man からそのまま持ってきました。
# Simple use of mktemp(1), where the script should quit
# if it can't get a safe temporary file:
TMPFILE=`mktemp /tmp/$0.XXXXXX` || exit 1
echo "program output" >> $TMPFILE
# Simple example, if you want to catch the error:
TMPFILE=`mktemp -q /tmp/$0.XXXXXX`
if [ $? -ne 0 ]; then
echo "$0: Can't create temp file, exiting..."
exit 1
fi |
テンポラリファイル名は、再利用しないでください(つまり削除して再作成します)。
いかに「安全な」テンポラリのファイル名を最初に得られたとしてもです。
攻撃者は、オリジナルのファイル名を見つけて、二度目に再利用する前に乗っ取って
しまうかもしれません。
もちろん適切なパーミッションを常にかけてください。
たとえば、誰でも、もしくはあるグループがそのファイルにアクセスする必要が
あるなら、そのアクセスだけを許可してください。さもなければ、モードを 0600 に
しておいてください(すなわち、所有者だけが読み書きできように)。
きちんと後始末をしてください。終了処理を使うか、UNIX ファイルシステムの実際
の処理方法を利用して、作成とともにファイルを unlink()してください。そうすると
ディレクトリ・エントリは消えてしまいますが、ファイル自体はファイルを指し示す
最後のファイル・ディスクリプタが閉じるまではアクセスできるようになっています。
そうすれば、プログラム内からはファイル・ディスクリプタ経由でファイルにアクセス
し続けられます。
ファイルを unlink するのは、コードをメンテナンスするのに非常に役立ちます。
ファイルはプログラムがクラッシュしたとしても自動的に削除されます。
すぐに unlink すると管理者がディスクスペースがどのくらいあるかがわかりにくく
なるという問題も多少はあります。それは単純に名前ではファイルシステムを見られ
なくなるからです。
環境変数の TMP や TMPDIR の値が確実に信頼できるところから得られ、コードが Unix
ライクなシステム向けなら、それらの環境変数を尊重してもよいかもしれません。
そうすれば、ユーザはテンポラリファイルをホームディレクトリ下のサブディレクトリ
のような共有していないディレクトリに移せます(そしてここで論じた問題を回避
できます)。
Bastille の最近のバージョンでは、ユーザ間で共有を減らすように、これらの変数を
設定できるようになっています。
残念ながら、ユーザは TMP や TMPDIR に共有ディレクトリ(たとえば /tmp)を設定
しているケースが多く、依然として安全が必要なプログラムでは、これらの環境変数
が設定してあっても、正しくテンポラリファイルを作成する必要があります。
GNOME の解決方法には長所が 1 つあります。少なくともあるシステムでは、
tempnam(3)は自動的に TMPDIR を利用しますが、mkstemp(3)で同様なことをするには、
さらにコードを書かなければならないからです。
テンポラリディレクトリ用にさらに環境変数(TEMP の ような)を作らないように
してください。特にアプリケーション毎に別の環境変数名を作らないでください
(たとえば、「MYAPP_TEMP」のように使わないこと)。
作成してしまうと、システム管理がとても複雑になってしまいます。特定のアプリ
ケーション用に専用のテンポラリファイルを望んでいるユーザが、そのアプリケー
ションを動かす時に環境変数を独自に設定できてしまいます。
もちろん、これらの環境変数が信頼できないソースで設定されてしまったなら、
これらを無視しなければいけません。Section 4.2.3 にある
アドバイスに従うなら、どのみちそうなるでしょう。
これらのテクニックは、テンポラリディレクトリが NFS version 2 (NFSv2)でマウント
した、リモートのディレクトリであるとうまく動きません。それは NFSv2 がきちんと
O_EXCL をサポートしていないからです。
詳しいことは Section 6.10.2.1 を見てください。
NFS version 3 以降では O_EXCL をきちんとサポートしています。テンポラリ
ディレクトリは、いつもローカルに作成するか、NFS を使ってマウントするなら、常に
NFS version 3 以降を使うのが解りやすい解決策です。
NFS v2 で安全にテンポラリファイルを作成するには、link(2) と stat(2)を使用
しますが、面倒です。これについては、Section 6.10.2.1 に
詳しい情報があります。
それはさておき、FreeBSD が最近になって mk*temp()系でファイル名に pid を
付けないようにした点は、注目に値します。pid ではなく、base-62 でエンコード
したランダムな値に完全に置き換えました。このことによって「デフォルト」の
6 文字分を使用したテンポラリファイルが大幅に増加しました。つまり、6 文字
を使った mktemp(3)でさえ、頻繁に使用しなければ、名前の推測に対してかなり
(確率的にも) 安全になりました。
しかしここでも教えにならうなら、彼らが取り組んでいる問題を回避するでしょう。
テンポラリファイルについての情報の多くは、
Kris Kennaway 氏が
2000 年 12月15日に Bugtraq へテンポラリファイルについて投稿した記事
によっています。