修正コード
Fedoraのリポジトリで実際に行われた修正を見てみましょう。
static void edit_cmd(void) {
......省略......
fclose(f);
if (fflush(NewCrontab) < OK) {
perror(Filename);
exit(ERROR_EXIT);
}
+ if (swap_uids() == -1) {
+ perror("swapping uids");
+ exit(ERROR_EXIT);
+ }
/* Set it to 1970 */
utimebuf.actime = 0;
utimebuf.modtime = 0;
utime(Filename, &utimebuf);
+ if (swap_uids_back() == -1) {
+ perror("swapping uids");
+ exit(ERROR_EXIT);
+ }
again:
rewind(NewCrontab);
if (ferror(NewCrontab)) {
fprintf(stderr, "%s: error while writing new crontab to %s\n",
ProgramName, Filename);
fatal:
unlink(Filename);
exit(ERROR_EXIT);
}
......省略......
done:
}
行頭に「+」をつけた部分が修正によって追加された部分です。プロセスの権限を一般ユーザ権限に落としてからutime()を実行しています。プロセスの実行権限を一般ユーザ権限に落しておけば、symlink攻撃を受けたとしても、システムファイルまでいじられる危険がなくなるというわけです。もちろん、当該ユーザの権限でいじれるものについては、これでは保護できていないことに注意が必要です。この修正は、symlink攻撃に対処するための最小限の修正だけを行なったものと理解すべきでしょう。
そもそもこのコードには、以下のような問題点があります。
- 一時ファイルのタイムスタンプをわざわざ0に設定している
タイムスタンプは編集作業によって更新されるので、ファイルが更新されたかどうかをチェックするには、生成時点のタイムスタンプを保存しておくだけでいいはずです。
- utime()によるファイル名文字列を使ったファイル操作
ファイルの中身をコピーするためにFILEポインタを使い、タイムスタンプを設定するためにファイル名文字列を使っています。ファイル名文字列を使ったファイル操作を行う場合には、今回のような競合状態が発生する危険があることに注意する必要があります。ファイル名文字列を使った操作は最小限にすること、また、ファイルをopenしたら、そのときに得られたFILEポインタやファイル記述子だけを使って操作を行うように心がけましょう。ファイルのタイムスタンプを設定するにはファイル名文字列を使ってutime()関数を使うしかありませんが、幸いにも今回のコードではタイムスタンプを設定する必要はありません。編集前のタイムスタンプを保存し、編集後のタイムスタンプと比較するような形にしたほうがより安全になります。
- root権限の使用(setuid root)
個々のユーザのcrontab(5)ファイルを保護するために、パーミション設定によるディレクトリの保護と、crontab(1)コマンドにおける権限管理を行なっていました。しかし、root権限はそのシステム上でなんでもできてしまうので、できるだけ使わないほうが望ましいのです。このような場合は、専用のユーザアカウントをひとつ設けて権限管理を行うことで、Cronに脆弱性があった場合でもシステムの他の部分への侵害を最小限に止めることが期待できます。
よりセキュアなcrontab(1)コマンド
Vixie Cronバージョン4、あるいはDebian/GNU Linuxや*BSDなどに取り込まれているCronのコードを調べてみると、上記に説明したような形で、よりセキュアなコードになっていることが分かります。また、権限管理もroot権限を使う代わりに、crontabという専用グループを設ける形で行っています。2種類のコードを比較してみることをお勧めします。
今回紹介した、ファイルの扱いやプロセスの権限管理に関連したガイドラインとして、セキュアコーディングスタンダードには、POS02-CやFIO01-Cといったレコメンデーションがあります。こちらもぜひ参考にしてみてください。
