第6章 セキュアC/C++ プログラミング
[6-4.]
サブシェル呼び出しは慎重に
system()関数を使って他のコマンドを起動し処理を委ねる場合,そのコマンド文字列に予期せぬ他のコマンドが紛れ込まないよう厳重に入力検査を行うことが重要である。また,起動するコマンドの参照をPATH環境変数に頼っていると,偽物のプログラムを起動させられ,システムを破壊されたり大切な情報を盗まれたりする恐れがある。



 PDF
既存コマンドの利用
プログラムの中で必要とされる一部の機能が,新たにコードを書くことなく既存のプログラム呼び出しさえすれば簡単に実現できるといった場面にときどき遭遇する。他のプログラムを呼び出すAPI には何種類かのものがあるが,その中でもコマンドラインとして投入するのと同じ形式の文字列を与えてやればそれを解釈実行してくれるsystem( )関数は使い方も簡単であり便利なものである。
 
ただ注意すべきことは,system( )関数は実はいわゆるサブシェル,すなわちシステムのコマンド解釈プログラム――Unix/Linux であればsh などのシェル,Windows であればCMD.EXE などのコマンドプロンプト――をサブルーチンとして呼び出してコマンドの処理をさせている点である。したがって,system( )関数はコマンド文字列に対して強力な解釈実行の力を持っているために,不用意に引数の文字列を与えると意図しない副作用が生じるおそれがある。
 
Unixにおけるサンプルプログラムをリスト1に示す。このプログラムは,コマンドライン引数に指定されたファイルの情報を「ls -l」コマンドを利用して表示するものである。15 行目でコマンドライン引数をもとにファイル情報表示用のコマンド文字列を作成し,19行目でそのコマンドを実行している。このプログラムの実行結果を実行例1に示す。想定どおりファイルの情報を出力しており,何の問題もないように思うかもしれない。しかし,実行例2を見ていただきたい。ここでは,引数に「testfile;echo hogehoge」を指定してプログラムを実行している。すると,ファイル「testfile」の情報が出力された後に「echo hogehoge」コマンドの実行結果が出力されてしまうのである。もし,コマンドライン引数が「testfile;rm -rf *」であったならば,プログラム実行ディレクトリ以下のファイルやディレクトリが全て消去(権限があれば)されることになる。
リスト1 ls コマンドを呼び出して利用するプログラム,cmdtest.c
  1 #include <stdio.h>
  2 #include <string.h>
  3 #include <stdlib.h>
  4 #include <ctype.h>
  5
  6 int main(int argc, char * argv[])
  7 {
  8   char command[256];
  9
 10   if (argc != 2){
 11     fprintf(stderr,"Usage %s filename\n",argv[0]);
 12     exit(1);
 13   }
 14
 15   if (snprintf(command, sizeof(command), "ls -l %s", argv[1]) > sizeof(command)-1){
 16     fprintf(stderr,"Length of the strings(filename) is too large.\n");
 17     exit(1);
 18   }
 19   system(command);
 20 }
実行例1 既存コマンドls を利用
 実行例1 既存コマンドls を利用
実行例2 別のコマンドも混入できてしまう
 実行例2 別のコマンドも混入できてしまう
このようにサブシェルを利用する場合,与えるコマンド文字列について用心が足りないと大きな危険を招きかねない。
外部から与えられたデータのチェック
サブシェルに渡すコマンドに外部から与えられたデータを含める場合はチェックが欠かせない。リスト2は,リスト1に入力チェックコードを追加したものである。18〜30行目で示される入力チェックでは,ファイル名として利用されるであろう文字(アルファベット,数字,ハイフン,アンダースコア,ドット)以外がコマンド引数に含まれていた場合,プログラムを終了している。この入力チェックによりシェル上で特別な意味を持つ文字を含むコマンド文字列がsystem( )関数に渡ることを防いでいる。リスト2の実行結果を実行例3に示す。コマンドライン引数として「testfile; echo hogehoge」が指定されているがエラーを表示してプログラムは終了しているのがお分かりいただけるだろう。
実行例3
 実行例3
PATH 環境変数の問題
リスト2に示されたプログラムには,まだ問題が残されている。それは,プログラムがPATH 環境変数に依存して作成されていることだ。指定されたコマンドの名前にディレクトリ区切り文字(UNIXでは/,Windowsでは\)が含まれておらず,シェルやCMD.EXE によって予約された名前でもない場合,PATH 環境変数に並べられたディレクトリから該当する名前のコマンドが検索される。
リスト2 データをチェックしてからサブシェルに与えるプログラム,cmdtest2.c
  1 #include <stdio.h>
  2 #include <string.h>
  3 #include <stdlib.h>
  4 #include <ctype.h>
  5
  6 int main(int argc, char * argv[])
  7 {
  8   char command[256];
  9   int i = 0;
 10   int len = 0;
 11
 12   if (argc != 2){
 13     fprintf(stderr,"Usage %s filename\n",argv[0]);
 14     exit(1);
 15   }
 16
 17   len = strlen(argv[1]);
 18   for (i = 0; i < len; i++){
 19     if (isalnum(argv[1][i]) == 0){
 20       switch(argv[1][i]){
 21       case '-'
 22       case '_'
 23       case '.'
 24         break;
 25       default
 26         fprintf(stderr,"illegal character.\n");
 27         exit(1);
 28       }
 29     }
 30   }
 31
 32   if (snprintf(command, sizeof(command), "ls -l %s", argv[1]) > sizeof(command)-1){
 33     fprintf(stderr,"Length of the strings(filename) is too large.\n");
 34     exit(1);
 35   }
 36   system(command);
 37 }
このような動作は特に問題ないように思えるかもしれない。しかし,大きな問題を孕んでいるのである。
 
Unix の場合,通常「ls」コマンドは「/bin」ディレクトリ下に存在する。そのため,PATH 環境変数に「/bin」ディレクトリが存在すれば,リスト2は正常に動作する。ところが,悪意あるユーザがPATH 環境変数を変更した場合のことを考えていただきたい。もし,PATH 環境変数から「/bin」ディレクトリの指定が取り除かれた場合,リスト2に示されたプログラムは正常に動作しなくなる。さらに,「/bin」以外のディレクトリがPATH 環境変数に含められ,そのディレクトリ下に「ls」という名前で偽のプログラムが置かれたとしたら,呼び出されたls プログラムはその呼び出した側の環境から見える資源に自由にアクセスでき,干渉することが可能になるのである。
 
とくに,呼び出した側がシステム管理者アカウントで動作するプログラムの場合,事態は深刻になる。特権ユーザの権限で動作するプログラムが他のコマンドを呼び出すとき,PATH環境変数に依存したコーディングは絶対にすべきではない。
PATH の値に依存しない呼び出し
PATH 環境変数の値に依存しないプログラム呼び出しを行うには,いくつかの方法がある。
絶対パス
1つめの方法としては,system( )関数に引数として渡すコマンドを絶対パスで指定することである。コマンドを「ls」と指定するのではなく,「/bin/ls」と指定する。こうすることで,コマンド実行時にPATH 環境変数による検索が行われなくなる。
PATH の再設定
2 つめの方法は,プログラム内でPATH 環境変数を再設定することである。この方法を実現するためには,setenv( ) (またはputenv( ))およびgetenv( )関数を利用する。
サブシェルを経由しない
3つめとして,コマンドの実行にそもそもサブシェルの使用を伴うsystem( )関数を利用するのではなく,直接対象のプログラムを起動するexec( )関数群(execle( )やexecve( )など)やWin32 のCreateProcess( )を使う方法が挙げられる。
 
ここでは,2つめの「プログラム内でPATH環境変数を再設定する」方法を利用してみよう。リスト2を改善したコードをリスト3に示す。39 行目で現在のPATH 環境変数を保存し,42 行目でPATH 環境変数の再設定を行っている。system( )関数実行後,47 〜 49 行目でPATH 環境変数を元に戻している。リスト3が正常に動作するかをテストするために,元のPATH環境変数を「/tmp」ディレクトリ下を優先的に検索するよう変更し,そのディレクトリ以下に不正な「ls」コマンドを用意した。実行例4が示すように,シェル上で「ls」コマンドを実行した場合は不正な「ls」コマンドが実行されているが,リスト3を実行すると本来のコマンド(「/bin/ls」)が実行されていることが分かる。
リスト3 PATH 変数を再設定するプログラム,cmdtest3.c
  1 #include <stdio.h>
  2 #include <string.h>
  3 #include <stdlib.h>
  4 #include <ctype.h>
  5
  6 int main(int argc, char * argv[])
  7 {
  8   char command[256];
  9   int i = 0;
 10   int len = 0;
 11   char *path;
 12
 13   if (argc != 2){
 14     fprintf(stderr,"Usage %s filename\n",argv[0]);
 15     exit(1);
 16   }
 17
 18   len = strlen(argv[1]);
 19   for (i = 0; i < len; i++){
 20     if (isalnum(argv[1][i]) == 0){
 21       switch(argv[1][i]){
 22       case '-'
 23       case '_'
 24       case '.'
 25         break;
 26       default
 27         fprintf(stderr,"You mast use illegal character.\n");
 28         exit(1);
 29       }
 30     }
 31   }
 32
 33   if (snprintf(command, sizeof(command), "ls -l %s", argv[1]) > sizeof(command)-1){
 34     fprintf(stderr,"Length of the strings(filename) is too large.\n");
 35     exit(1);
 36   }
 37
 38   // 現在の環境変数を保存
 39   path = getenv( "PATH" );
 40
 41   // 環境変数を設定
 42   setenv("PATH","/bin",1);
 43
 44   system(command);
 45
 46   // 環境変数を元に戻す
 47   if (path != NULL){
 48     setenv("PATH", path, 1);
 49   }
 50 }
実行例4
 実行例4
まとめ
既存のコマンドをsystem( )関数で呼び出して新たなプログラムの部品として利用する場合,与えるコマンド文字列に含めるデータは十分なチェックをパスしたものでなくてはならない。それらのデータには,背後で起動されるサブシェルにとって特別な意味をもつ文字が含まれている可能性があるからだ。また,PATH環境変数に依存したプログラム呼び出しも避けなければならない。悪意あるユーザがPATH 環境変数を書き換えているおそれがあるからだ。
参考文献
『UNIX & インターネットセキュリティ第2版』,Simon Garfinkel,Gene Spafford共著,山口英監訳,谷口功訳,オライリー・ジャパン