田中 哲
Untrusted search path vulnerability は「信用できない検索パスの脆弱性」という意味である。代表例としては環境変数 PATH に .(カレントディレクトリ)を入れた場合の問題が古くから知られているが、これに限られたものではない。2009年上期には、Python に関連したUntrusted search path vulnerabilityが多数出たことを踏まえ、これを解説する。
Python は動的オブジェクト指向プログラミング言語である。Pythonはアプリケーションに組み込むことにより、アプリケーションをPythonで拡張することができる。実際、vim やblenderなど多くのアプリケーションがPythonの組み込みをサポートしている。
昨年から、Python関連のUntrusted search path vulnerabilityとして以下のように一連のCVEが出ている。
これらのCVEは、Debian BTSの以下の報告が発端である。
この問題は、カレントディレクトリに存在するPythonスクリプトをアプリケーションが勝手に (ユーザが意図していないのに)読み込んで実行してしまうというものである。
一般に、カレントディレクトリに存在するスクリプトは実行して安全なものとは限らない。安全でないディレクトリの代表例はマルチユーザ環境における/tmpである。/tmpは任意のユーザがファイルを作れるため、/tmp以下にある他ユーザのファイルを意図せず実行してしまうと、その他ユーザは実行したユーザに危害を及ぼせる。またシングルユーザ環境でも、ダウンロードしてきたソフトウェア(tarball)を展開した内部は/tmpと同様に危険である。提供元はtarball中に任意のファイルを用意できるので、任意のスクリプトを実行してしまう可能性がある。悪意を持って用意されたスクリプトを実行してしまった場合、ホームディレクトリの削除や秘密鍵を盗まれるなど、さまざまな問題が生じうる。さらに、そのアプリケーションが管理者権限で実行されていたとすると、ホスト全体に危険が及ぶ。
Pythonにはコマンドライン引数を表すsys.argvがある。これはsysモジュールのargv という変数であり、以下のように表示できる。ここではimport sysによってsysモジュールを使用することを宣言し、sys.argvをprintによって表示している。
% cat tst.py import sys print sys.argv % python tst.py foo bar ['tst.py', 'foo', 'bar'] |
sys.argvは上記のように文字列のリストである。先頭要素がスクリプトの名前となり、それ以降の要素が個々の引数となる。
また、Pythonがモジュール(ライブラリ)を探索するパスとしてsys.pathが用意されている。これはsysモジュールのpathという変数であり、以下のように表示できる。
% cat tst.py import sys print sys.path % python tst.py ['/home/akr', '/usr/lib/python2.5', '/usr/lib/python2.5/plat-linux2', ...] |
sys.pathは上記のようにディレクトリを要素とするリストである。Pythonはライブラリを探索する場合、sys.pathの要素を先頭から順に使用する。
ここで、sys.pathの先頭の /home/akrは、スクリプト(tst.py)の存在するディレクトリである。Python は起動したスクリプトの存在するディレクトリをsys.pathの先頭に挿入する。これにより、スクリプトと同じディレクトリに存在するモジュールを簡単に使用することができる。
アプリケーションがPythonを組み込むにあたって、複数のアプリケーションが、PySys_SetArgv関数を用いていた。
ここではgeditのコードを示す。
char *argv[] = { "gedit", NULL };
PySys_SetArgv (1, argv);
|
PySys_SetArgv関数は、sys.argvを設定する関数である。呼び出さない状況では sys.argvは存在しないが、上記のように呼び出すと ['gedit'] と1要素のリストに設定される。sys.argvの先頭要素はスクリプトの名前であるから、この呼び出しはスクリプトの名前を設定することになる。
ここで、PySys_SetArgv関数は sys.argvの設定に加え、sys.pathの先頭にスクリプトが存在するディレクトリを挿入するという効果がある。Pythonはこのディレクトリを求めるにあたり、まず"gedit"というパスを絶対パスに変換し、そのファイル名部分を取り除いてディレクトリを得る。しかし、ファイルが存在しない場合、"gedit"という文字列をパスとみなしてそのファイル名部分を取り除く。そのため、取り除いた結果は空文字列となり、Python は空文字列を sys.path の先頭に挿入する。 空文字列はカレントディレクトリを意味するため、これによりPythonはモジュールをカレントディレクトリから探索するようになる。
この状態でgeditがなにかモジュールを使用すると、そのモジュールはまずカレントディレクトリで探される。つまり、Pythonが標準で提供しているモジュールであっても、標準のディレクトリが探されるよりも前にカレントディレクトリが探され、カレントディレクトリにその名前のモジュールがあればそれが使われてしまう。
この問題は、通常のPythonスクリプトには影響しない。通常のPythonスクリプトが /usr/binや /usr/local/binなど、管理者しか変更できないディレクトリにインストールされて実行された場合、sys.pathの先頭には /usr/binや /usr/local/binというディレクトリが挿入される。それらのディレクトリは管理者しか変更できないため、任意のユーザが悪意のあるファイルを置くことはできない。同様に ~/bin など、個人的なディレクトリにインストールした場合も他のユーザが書き込むことはできないので安全である。
カレントディレクトリがsys.pathに挿入されるのは、Pythonを組み込んだアプリケーションがPySys_SetArgv関数を使って"gedit"などといったスクリプト名を設定したときや、python を対話的に実行した場合、また、pythonの -cオプションを用いてコマンドライン引数にスクリプトを記述して実行した場合である。geditの例では、ユーザがpythonを実行していることを意識している可能性は低いが、明示的にpythonコマンドを起動した場合はユーザは python を使用することを意識している。このため、カレントディレクトリの危険性は gedit のような組み込み python のケースのほうが大きい。
ここで、Python を組み込んだアプリケーションに注目して脆弱性を解決するとすると、方法はいくつか考えられる。
Python 本体で対応するのは本質的である。ひとつの修正で確実に脆弱性を修正できる。
しかし、PySys_SetArgv関数がsys.pathを変更することに依存したアプリケーションがもし存在したとすると、そのようなアプリケーションに不具合が生じるという副作用がある。
各アプリケーションで対応してカレントディレクトリを取り除くのであれば、例えば以下のようにできる。
char *argv[] = { "gedit", NULL };
PySys_SetArgv(1, argv);
PyRun_SimpleString("import sys; sys.path = filter(None, sys.path)");
|
ここでは PySys_SetArgvを呼んだ後に import sys; sys.path = filter(None, sys.path) というちいさな Python スクリプトをPyRun_SimpleString 関数で実行している。filter(None, sys.path)は sys.pathの要素のうち、偽の要素を取り除いたリストを返す。Python では空文字列 '' は偽なので、空文字列 (カレントディレクトリ)を取り除くことになる。
しかし、いちど付け加えたものをわざわざ取り除くというのはあきらかに無駄である。しかも Python スクリプトを使うのでインタプリタの解釈実行の手間もかかる。
また、argv の先頭要素として、/usr/bin/geditなど、安全なディレクトリ下のファイルを指定するという方法もとれる。しかし、安全なディレクトリを選ぶ必要があるし、/usr/bin という余計なディレクトリがsys.pathに含まれることになる。
これらの無駄を避ける明らかな方法はPySys_SetArgvの呼び出しを除去することである。そうできれば、sys.pathを書き換えることもなく、脆弱性も修正できる。しかし、これには副作用があり、sys.argvが設定されなくなってしまう
副作用を起こさずに無駄を避けるには、Pythonとアプリケーションの両方で対応が必要である。sys.argvを設定するが sys.pathは変更するかどうか選べる PySys_SetArgvEx 関数をPythonに追加し、アプリケーションがそれを使うようにすれば、無駄も副作用もなく脆弱性を修正できる。
ここでどの方法を選ぶかという問題が生じる。
もっとも無駄がないのは、各アプリケーションでPySys_SetArgvの呼び出しを除去することである。しかし、これにはsys.argvが設定されなくなるという副作用があり、そのことに実害があるかどうかが問題になる。ここで、sys.argvを設定するとはいっても、geditの例のように固定した値を設定しているだけである。もし、何らかの意味のある値を設定しているのであれば、アプリケーション側から Python スクリプト側に情報を伝達するという意味があるが、固定された値ではそのような用途とは考えられない。そのため、ここでPySys_SetArgvを呼び出す意味や意図ははっきりしない。したがって、単に消してしまっても問題ないという可能性がある。
ここでPythonのあるコントリビュータ(Gregory P. Smith)がgeditのメンテナ (Jesse van den Kieboom)に尋ねたところ、その部分は「vimからコピーしてきた」ということが判明した。
そして、vimのソースコードを確認すると以下のようにコメントがついており、「warn() がクラッシュする」ことを避けるのがPySys_SetArgvを呼び出す理由であったことが判明した。
/* Set sys.argv[] to avoid a crash in warn(). */
PySys_SetArgv(1, argv);
|
ここで warn() は、おそらくPythonに標準添付されている warnings モジュールの warn 関数を指すと考えられる。Python 2.3以前ではsys.argvが設定されていない状況で warn 関数を呼び出すと AttributeError が発生していた。warn() が sys.argv に依存している理由は、出力する警告の中にスクリプトの名前を含めるためである。そのために warn() は sys.argv[0] にアクセスするが、sys.argvが存在しない場合、AttributeErrorが発生する。この問題は現在では修正されており、修正後のwarn() は AttributeErrorを捕捉して、(スクリプトの名前は表示されないものの)警告自体は出力されて、例外は発生しないようになっている。[2]
つまり、アプリケーションが PySys_SetArgv関数を呼び出すのには過去には確かに意味があった。そして、warn()以外にもsys.argvを参照しているコードが存在する可能性は現在でも存在するため、呼び出しを削除するのは単純に正しいとはいえない。
このような点があり、Debianでは、多くのアプリケーションについて
PyRun_SimpleString("import sys; sys.path = filter(None, sys.path)");
|
というコードを追加する対策がとられた。
なお、Python自体の修正については、2009年7月時点では議論の結論が出ていない。[1]
一般的な話として、Untrusted search path vulnerability を防ぐ最善の方法は「カレントディレクトリはサーチパスに入れない」ということである。
しかし、開発中のソフトウェアをテストするときなど、カレントディレクトリがサーチパスに入っていると便利なこともある。これは、ライブラリを開発している場合、カレントディレクトリがサーチパスに入っていれば、そのライブラリをインストールせずに使用できるからである。インストール不要でテストできることは開発のターンアラウンドタイム (ソースコードの編集から実行・テストまでにかかる時間)を短縮し、開発効率を向上する。
ここでセキュリティと開発効率を両立させる方法としては、ファイルからの相対指定があげられる。
この例としては、C言語の #include "..." という指示がある。この指示はソースファイル中に他のファイルを取り込むことを意味する。ここで指定したファイル名をどのように探索するかは処理系の実装依存であるが、伝統的にはこのファイル名はこの指示が記述されているファイルからの相対パスとして解釈される。カレントディレクトリはその解釈には関わらない。
Python でも同様な相対importの機能が使用できる。[4]
このような相対指定を用いれば、ライブラリが複数ファイルに分かれていても、それらのファイル内から他のファイルをインストール済みの状態と同じく参照できる。したがって、カレントディレクトリがサーチパスに含まれていなくても、インストールしていないソフトウェアをテストすることが容易になる。
プログラミング言語の側から脆弱性を考えると、適切な修正方法を判断するのは困難なことがある。
適切な方法を判断するためには、記述されたプログラムの意図を調べることが必要であるが、これは必ずしも容易ではないことがわかる。この事例においては、vimのソースコードにコメントがあったために意図が判明したが、コメントがなければプログラマに尋ねる必要があったと考えられる。そして、尋ねたとしてもプログラマが意図をはっきりと覚えているとは限らない。したがって、プログラムの意図は結局わからないという結果も十分にありえる。
言語処理系には、脆弱性を生んでしまったコードが本来意図していた問題解決を、脆弱性を生まない形で適切に解決する方法を用意することが期待される。しかし、もともとのコードが想定していた問題を知ることができなければ、その期待に適切に答えることは難しい。
Untrusted search path vulnerabilityについていえば、カレントディレクトリをサーチパスに入れないことが最善であるが、それは利便性の低下を伴う。利便性とセキュリティを両立させるためにはファイルからの相対指定を利用できる。
以上
| [1] | CVE-2008-5983 python: untrusted python modules search path, 2009-04-14, |
| [2] | attempt to access sys.argv when it doesn't exist, 2003-11-10, |
| [3] | Debian BTS #493937 - bicyclerepair: bike.vim imports untrusted python files from cwd, 2008-06-03, |
| [4] | PEP 328 -- Imports: Multi-Line and Absolute/Relative, 2007-06-19, |