4.2. 環境変数

環境変数は、デフォルトでは親プロセスから継承されます。 しかしあるプログラムから別のプログラムを実行(exec)した場合、環境変数に 任意の値を設定できます。 setuid や setgid されたプログラムでは、これは危険です。というのも、 プログラムを呼び出すことで環境変数のコントロールが可能になり、環境変数を 別のプログラムに渡せてしまうからです。 通常、環境変数は継承されてしまうため、この危険性も同時に引き継がれてしまい ます。安全性が必要なプログラムが他のプログラムを何も考えずに呼び出すと、 もしかすると危険である環境変数の値がそのプログラムが呼び出すプログラムに 渡されてしまうかもしれません。 下記のサブセクションでは、環境変数とその取り扱いについて論じます。

4.2.1. 環境変数の中には危ないものもある

環境変数の中には危険なものがあります。理由は、たいていの場合ライブラリや プログラムは環境変数によってコントロールされているものの、その方法が あいまいだったり、わかりにくかったり、ドキュメント化されていないものが あったりするからです。 たとえば IFS 変数は、shbash でコマンドラインの引数を 分割するために使用するキャラクタの指定で利用されています。 シェルは低レベルのシステムコール(C の system(3) や popen(3)や Perl の backtick 演算子)を利用して呼び出されるため、IFS 変数に 異常な値を設定すると、一見安全と思われるシステムコールを危険なものに 変えてしまう恐れがあります。 この動作は bash や sh のドキュメントに載っていますが、はっきりとは書いて ありません。 長年愛用している人だけが、IFS を知っています。それは IFS を使うと セキュリティが脅かされるという理由であって、本来の目的で実際によく使われる からではありません。 さらに困ったことに、すべての環境変数がドキュメント化してあるわけではなく、 ドキュメント化してあったとしても、その他のプログラムが値を変更したり、 危険な環境変数を追加してしまう可能性もあります。 つまり、唯一の解決方法(下記にあげますが)は、必要な環境変数を選び出し、残りを 捨てることです。

4.2.2. 環境変数の保存方法にまつわる危険性

本来プログラムは、標準的なアクセス手段で環境変数を利用した方がベターです。 たとえば、C では値を取得するのに getenv(3)を使い、値を設定するのに POSIX 規格である putenv(3)を使うか、BSD の拡張である setenv(3)を使います。 また環境変数を削除するのには、unsetenv(3)を使います。 ここで強調しておきたいのは、setenv(3)が Linux でも実装されていることです。

しかし、攻撃者はそのように行儀よくする必要はありません。攻撃者は環境変数 のデータ領域を直接コントロールし、そのデータ領域を execve(2)を使った プログラムに渡せます。これが悪意ある攻撃を可能にしています。この攻撃は 環境変数が実際にどのように動作するかを理解してはじめて理解できる攻撃です。 Linux では environ(5)を見れば、要約したかたちで環境変数がどのように機能 するかがわかります。 簡単に言えば、環境変数は文字に対するポインタの配列へのポインタとして記憶して います。この配列は規則正しく並んでいて、NULL ポインタで終端してあります (したがって、配列の最後がわかります)。同様に文字へのポインタは、それぞれ が NIL で終端してある「名前=値」というフォーマットの文字列の値をそれぞれ 指しています。 これが言わんとすることは、いくつかあります。たとえば環境変数名には、 イコール記号(=)を入れることができません。名前だけでなく値にも NIL 文字を 埋めこめません。 しかし危ないと言う意味では、同じ名前でありながら値が異なる複数のエントリ (たとえば、複数の SHELL 変数値)を認めるというものもあります。 代表的なコマンドシェルは、これを禁止していますが、攻撃者がローカルで作業して いれば、execve(2)を使ってそのような状況を作れます。

この書式を記録(設定する方法も)する上での問題は、プログラムが (正しい値であるかを見るために)これらの値の 1 つをチェックすればよいのに、 実際は違うものを使ってしまう点にあります。 Linux では GNU glibc ライブラリがこの問題からプログラムを保護することに 取り組んでいます。 glibc 2.1 における getenv の実装は、常に最初にマッチした項目を取得し、 setenv と putenv は常に最初にマッチした項目に設定します。unsetenv は、 マッチした項目 すべてを解除します(GNU glibc の実装は何と素晴らしいことか!)。 しかし、プログラムには環境変数に直接アクセスし、環境変数すべてをなめるものも あります。この場合は、プログラムが最初ではなく、最後にマッチした項目を使う 可能性があります。 となると、最初の項目をチェックしているにもかかわらず、実際は最後の項目を 使ってしまうことになります。攻撃者はこの事実を利用して、保護ルーチンを回避 してしまいます。

4.2.3. 解決方法――選別して、消し去る

setuid や setgid してあるプログラムを安全にするには、入力として必要な 環境変数(があれば)を減らして、周到に選別しなければいけません。 そして環境変数全体を消し、その後必要となるわずかな数の環境変数に、安全な 値を再設定します。 何らかの下位プログラムを呼び出すなら、これが一番優れた方法です。 「危険な値をすべて」をリストアップする手法は、実質的ではありません。 直接間接に呼び出すプログラムごとにソースコードをレビューしたとしても、 あなたがコードを書いた後に、ドキュメント化していない新たな環境変数を 誰かが追加してしまうかもしれません。そういうものの 1 つが危険であるかも しれません。

C や C++ で簡単に環境を消去する方法は、グローバル変数の environ に NULL を設定してしまう方法です。 グローバル変数である environ は <unistd.h>; で定義してあり、C や C++ ユーザはこのヘッダーファイルを #include する必要があります。 まずこの値を処理してから、スレッドを立ち上げなければいけませんが、それが 問題となることはめったにありません。というのは、プログラムを実行する際、 できるだけ早い段階(通常はスレッドを起こす前)でこれらの処理を行う必要がある からです。

グローバル変数 environ はさまざまな規格で定義してあります。公式な規格で 直接値を変更することを認めているかどうかははっきりしません。しかし直接値を 変更することで問題が発生してしまうような Unix ライクなシステムを私は知りません。 私は通常「environ」だけを直接修正しています。 そのような低レベルの構成要素で処理すると、おそらく互換性はなくなります。しかし 確実にクリーン(で安全)な環境を得ることが保証されます。 環境変数全体に対して、後になってアクセスが必要なケースが時として生じる ので、「environ」変数の値をどこか別のところに保存しておくのもよいでしょう。 しかし、プログラムにはほんのいくつかの値が必要な場合がほとんどなので、残りは 落とすほうがよいでしょう。

もう 1 つ環境をクリアにする方法があります。それは clearenv()というドキュメント にのっていない関数を使う方法です。 この clearenv()関数は生い立ちが変わっています。POSIX.1 では定義されることに なっていましたが、どういうわけか規格には入りませんでした。 しかし、clearenv()は POSIX.9(Fortran 77 の POSIX 規約)で定義しているので、 準公式扱いになっています。 Linux で clearenv()は、<stdlib.h> で定義してありますが、#include を使って取り込む前に、必ず __USE_MISC が #defined していなければいけません。 もう少し「公式」なアプローチとして __USE_MISC を定義するのに、まず _SVID_SOURCE もしくは _BSD_SOURCE をまず宣言してから、 #include <features.h> してください。これらはテスト用マクロとして公式 の機能です。 __

PATH は、追記されるタイプの環境変数の 1 つです。ディレクトリのリストになって いて、プログラムを検索するのに使用します。PATH には、カレントディレクトリを 含めてはいけません。普通は単純に 「/bin:/usr/bin」という感じにします。 IFS(デフォルトは「 \t\n」で、最初の文字はスペースです) や TZ(タイムゾーン)も 設定しているかもしれません。 Linux は IFS や TZ を設定していなくても、止まることはありません。しかし、 System V ベースのシステムの中には、TZ に値を設定しないと問題が起こるものも あります。また、IFS に値を設定していないとまずいシェルがあるようです。 Linux では environ(5)を見て、一般的な環境変数の一覧を確認し、 設定したい変数を見つけた方がよいでしょう

ユーザが提供する値を本当に必要とするなら、まず値をチェックしてください(値が 正式な値のパタンにマッチしているか、許容している最大文字列長を超えて いないかを確認してください)。 /etc に、「安全な環境変数の基準」を示した、信頼できる基準となるファイルが存在 するのが理想です。しかし現状は、この目的に合致した基準となるファイルは存在 しません。 似たようなものとして、もしシステム上に PAM モジュールがあるなら、pam_env を調べた方がよいでしょう。

シェルをプログラミング言語として使っているなら、「/usr/bin/env」に「-」 オプションを利用します(これで動作するプログラムの環境変数すべてが削除 されます)。 つまり、/usr/bin/env を「-」オプション付きで呼び出して、その後に変数を 準備し、それに値を設定します(名前=値の形で)。 次に、プログラムに引数を与えて起動します。 通常はプログラムをフルパス(/usr/bin/env)で指定してください。ただ「env」 としないでください。そうするとユーザが危険な PATH の値を作成してしまい ます。 GNU の env には「-i」とその同義である「--ignore-environment」(これもプロ グラムが起動すると環境変数を削除する)がありますが、他のバージョンとは互換性 がありません。

setuid や setgid するプログラムを作成していて、その開発言語が環境を直接再設定 できないなら、「ラッパー」プログラムを作成するのも手の 1 つです。 ラッパーは、プログラムの環境を安全な値に設定し、他のプログラムを呼び出します。 注意しなければいけないのは、ラッパーが対象となるプログラムを実際に呼び 出さなければいけない点です。 そのプログラムがインタプリターなものなら、競合状態に絶対に陥らないように してください。競合状態が起こると、特別に権限を許可して setuid や setgid して あるプログラムではなく、別のプログラムをインタプリターがロードしてしまうかも しれないからです。