安全なウェブサイトの作り方の自分用のまとめです。
失敗例
SQL インジェクションの例
PHPとPostgreSQLの組み合わせ
【脆弱な実装】
$query = "SELECT * FROM usr WHERE uid = '$uid' AND pass = '$passh';
$result = pg_query($conn, $query);
上記のように、ウェブアプリケーションに外部から渡されるパラメータに対してエスケープ処理を行っていない場合、想定外の SQL 文を実行させられる原因となる。
【解説】
たとえば、ユーザ ID にtaro'--
という文字列が与えられた場合、ウェブアプリケーションがデータベースに要求する SQL 文は下記のようになる。
SELECT * FROM usr WHERE uid = 'taro'--' AND pass ='eefd5bc2...'
上記 SQL 文中のシングルクォート'
は、文字列定数を括る「引用符」の意味を持つ特別な文字である。また、ハイフンの繰り返し--
は、それ以降の内容をコメントとして無視させる意味をもつ。このため、この文字列が与えられた場合、データベースは' AND pass = eefd5bc2...
を無視する。
この結果、データベースで実行される SQL 文は、下記のようになる。
SELECT * FROM usr WHERE uid = 'taro'--
これは、仮に「taro」というユーザが存在していた場合、「taro」のパスワードを知らなくてもログインが可能であることを意味する。認証回避だけでなく、$uid に与える文字列を変えることで、攻撃者は自由にデータベースを操作することができてしまう場合がある。SQL 文を構成する要素に対し、エスケープ処理を施していないことが、本問題の原因である。
【修正例1】 プリペアドステートメントを使う
pg_query() の代わりに、pg_prepare() および pg_execute() を利用する。
$result = pg_prepare($conn, "query", 'SELECT * FROM usr WHERE uid= $1 AND pass=$2);
$result = pg_execute($conn, "query", array($uid, $passh));
【修正例2】 プレースホルダの仕組みを持つ関数を利用
pg_query() の代わりに、pg_query_params() を利用する。
$result = pg_query_params($conn, 'SELECT * FROM usr WHERE uid = $1
AND pass = $2', array($uid, $passh));
【修正例3】 専用のエスケープ関数を利用
pg_escape_string() を利用し、pg_query() で実行する SQL 文中の全ての変数要素に対してエスケープ処理を行う。
$query = "SELECT * FROM usr WHERE uid = '".pg_escape_string($uid)."'
AND pass = '".pg_escape_string($passh)."'";
$result = pg_query($conn, $query);
OSコマンド・インジェクションの例
Perl から sendmail コマンドの呼び出し
【脆弱な実装】
$from =~ s/"|;|'|<|>|\|| //ig;
open(MAIL, "|/usr/sbin/sendmail -t -i -f $from");
これは、ウェブのフォームに入力されたメールアドレスを差出人としてメールを送信するプログラムの一部である。
$from に、入力された差出人アドレスが格納されている。1 行目は、シェルのコマンドライン上で特別な意味を持つ文字である"
と;
、'
、<
、>
、|
、スペース
を$from から削除しようとしている。2行目は、OS の sendmail コマンドを呼び出して、メールを送信する処理を開始し、差出人アドレスとして$from の値をコマンドライン引数に渡している。この実装は、1 行目の処置を施してもなお、OS コマンド・インジェクション攻撃に対して脆弱である。
【解説】
この実装で、$from の値が「someone@example.jp」であるならば、次のコマンドが実行され、これは正常に処理される。
/usr/sbin/sendmail -t -i -f someone@example.jp
しかし、攻撃を意図した入力により、$from の値が「touch[0x09]/tmp/foo
」(ここで「[0x09]」は水平タブを表す)となった場合、次のコマンドが実行され、OS コマンド・インジェクションの脆弱性を突いた攻撃が成立してしまう。
/usr/sbin/sendmail -t -i -f `touch[0x09]/tmp/foo`
【修正例 1】 ライブラリを使用する方法
コマンドの呼び出しをやめることで、OS コマンド・インジェクションの脆弱性に対する根本的解決となる。コマンドの呼び出しで実現していた機能を、既存のライブラリを用いて実現できないか検討する。
use Mail::Sendmail;
%mail = (From => $from, …);
sendmail(%mail);
【修正例 2】 コマンドライン中に値を埋め込まない方法
ライブラリの利用が難しく、コマンドを使わざるを得ない場合でも、コマンドの呼び出し方を変更することで、OS コマンド・インジェクションの脆弱性を解消できる場合がある。
$from =~ s/\r|\n//ig;
open(MAIL, '|/usr/sbin/sendmail -t -i');
…
print MAIL "From: $from\n";
例題コードの場合、メールの差出人を指定する部分が問題となっていたが、sendmail コマンドでは、差出人は必ずしも引数で指定する必要はない。上記のようにコマンドの標準入力に与えるメールデータのヘッダに差出人を指定することができます。この方法ならば、コマンドライン中に値を埋め込むことを避けられるので、OS コマンド・インジェクションの脆弱性を解消できる。
ただし、この修正例のように、メールヘッダを出力する際には、メールヘッダ・インジェクションの脆弱性に注意が必要で、出力する $from に改行コードが含まれないようにしなければならない。
【修正例 3】 シェルを経由せずにコマンドを呼び出す方法
ライブラリの利用が難しく、コマンドを使わざるを得ない場合でも、シェルを経由せずにコマンドを呼
び出すことで、OS コマンド・インジェクションの脆弱性を解消できる場合がある。
open(MAIL, '|-') || exec '/usr/sbin/sendmail', '-t', '-i', '-f', '$from';
パス名パラメータの未チェックの例
PHP によるファイル内容の画面表示
【脆弱な実装】
$file_name = $_GET['file_name'];
if(!file_exists($file_name)) {
$file_name = 'nofile.png';
}
$fp = fopen($file_name,'rb');
fpassthru($fp);
これは、指定された名前のファイルの内容を画面に表示するプログラムの一部である。ここでは、指定されるファイルはサーバの公開ディレクトリ上にあるファイルのみと想定している。
この実装は、URL 中で指定されるファイル名に、絶対パス名や、../
を含むパス名が与えられる可能性があることを考慮していないため、ディレクトリ・トラバーサル攻撃に対して脆弱である。
【解説】
この実装では、URL 中の file_name パラメータで/etc/passwd
を指定された場合、/etc/passwdの内容を画面に表示してしまう。
また、下記のように、ディレクトリをプログラム中で指定することにより、URL で絶対パス名を指定されることを防止したとしても、URL で../../../etc/passwd
のように、上位ディレクトリを辿る相対パス名を指定されると、/etc/passwd の内容を表示してしまう。
$file_name = $_GET['file_name'];
$dir = '/home/www/image/'; //ディレクトリを指定
$file_path = $dir . $file_name;
if(!file_exists($file_path)) {
$file_path = $dir . 'nofile.png';
}
$fp = fopen($file_path,'rb');
fpassthru($fp);
【修正例】 パス名からファイル名だけを取り出して使用する
OS や言語に用意されている機能を用いて、パス名からファイル名部分だけを取り出して使うようにすることで、パス名パラメータに関する脆弱性に対する根本的解決となる。
$dir = '/home/www/image/';
…
$file_name = $_GET['file_name'];
…
if(!file_exists($dir . basename($file_name))) {
$file_name = 'nofile.png';
}
$fp = fopen($dir . basename($file_name),'rb');
fpassthru($fp);
不適切なセッション管理の例
Perl によるセッションIDの生成
【脆弱な実装】
sub getNewSessionId {
my $sessid = getLastSessionId ('/tmp/.sessionid');
$sessid++;
updateLastSessionId ('/tmp/.sessionid', $sessid);
return $sessid;
}
これはセッション ID を発行するプログラムの一部です。このプログラムでは getNewSessionId 関数を呼び出してセッション ID を生成しており、getNewSessionId 関数では、ファイル /tmp/.sessionidに保存している数値を 1 ずつ増やしながらセッション ID を返す。
この実装では、セッション ID が推測可能となる脆弱性がある。
【解説】
この実装では、セッション ID を数値で表しており、1 から始めて、2、3、4 と連番で発行している。このプログラムでは、最後に発行したセッション ID をファイル /tmp/.sessionid で管理している。攻撃者がこのサイトを利用すると、攻撃者にもセッション ID が発行される。たとえば、攻撃者が取得したセッション ID が「3022」であったなら、そのタイミングで「3021」のセッション ID が有効である可能性がある。攻撃者は、セッション ID として「3021」を送信してサイトにアクセスすることで、セッション ID「3021」が割り当てられている他の利用者のセッションを乗っ取ることができてしまう。
こうしたセッション・ハイジャック攻撃を防止するために、セッション ID は、暗号論的擬似乱数生成器を用いて生成するべきである。
クロスサイト・スクリプティングの例
エスケープ処理の未実施
【脆弱な実装】
use CGI qw/:standard/;
$keyword = param('keyword');
...
print ... <input name="keyword" type="text" value="$keyword">
... 「 $keyword」の検索結果...
上記ソースコードは、検索結果の表示処理の一部である。
検索フォームに入力された文字列IPA
はウェブアプリケーションに送信され、$keyword に格納される。このウェブアプリケーションは、検索結果をウェブページとして出力する際、この $keyword を、フォーム内や見出し等、複数の場所に埋め込んでいる。しかし、この $keyword に対して、出力前にエスケープ処理を行っていないため、スクリプトを埋め込まれる原因となる。
【解説】
ウェブアプリケーションが文字列を出力する際には、それをテキストとして出力するのか、HTML タグとして出力するのかによって、行うべき処理が異なる。この例の場合、$keyword は検索キーワードであり、テキストとして出力するべき要素である。したがって、$keyword に含まれる&
、<
、>
、"
、'
等に対して、エスケープ処理を行う必要がある。
この処理を怠ると、$keyword にこれらの文字を含む文字列が指定されることにより、開発者の意図に反して画面が崩れる不具合が生じる。この不具合を悪用した攻撃手法が、クロスサイト・スクリプティング攻撃である。
【修正例1】 エスケープ用の関数を利用
CGI モジュールの escapeHTML() を利用する。
use CGI qw/:standard/;
$keyword = param('keyword');
...
print "<input ... value=\"".escapeHTML($keyword)."\"...";
print "「 ".escapeHTML($keyword)."」の検索結果...";
escapeHTML() は、引数に指定された文字列に含まれる、HTML において特別な意味を持つ文字に対してエスケープ処理を行い、その結果を返す。下記は、escapeHTML() におけるエスケープ対象文字と、その処理結果である。
対象文字 | 処理結果 |
---|---|
& | & |
< | < |
> | > |
" | " |
' | ' |
【修正例2】
独自に作成したエスケープ処理関数を使用する。
print "<input ... value=\"".&myEscapeHTML($keyword)."\"...";
print "「 ".&myEscapeHTML($keyword)."」の検索結果...";
...
# 独自に作成したエスケープ処理関数 myEscapeHTML
sub myEscapeHTML($){
my $str = $_[0];
$str =~ s/&/&/g;
$str =~ s/</</g;
$str =~ s/>/>/g;
$str =~ s/"/"/g;
$str =~ s/'/'/g;
}
文字コードの未指定
【脆弱な実装】
ウェブアプリケーションの応答結果
HTTP/1.1 200 OK
...
Content-Type: text/html
<HTML>
<HEAD>
<META http-equiv="Content-Type" content="text/html">
上記は、あるウェブアプリケーションの応答結果の一部である。
「Content-Type」フィールドの値は、送信されるデータの種類(メディアタイプ)をウェブブラウザに判定させるために利用する情報である。しかし、上記には、文字コード(charset)を判別するための情報が指定されていない。この場合、ウェブブラウザは、独自の実装に基づく文字コード判定(たとえば、受信したデータの内容から文字コードを推測する)を行うが、この挙動がクロスサイト・スクリプティング攻撃に悪用される場合がある。
【解説】
本例は、ウェブブラウザにおける独自の文字コード判定機能を悪用したクロスサイト・スクリプティング攻撃の対策が未実施である例である。この解決には HTTP レスポンスヘッダの「Content-Type」フィールドに文字コードを指定する必要がある。
【修正例】HTTP レスポンスヘッダの「Content-Type」フィールドに文字コードを指定
HTTP/1.1 200 OK
...
Content-Type: text/html; charset=UTF-8
対策漏れ
テキスト形式で入力される値のみに入力段階でエスケープ処理
【脆弱な実装】
本例は、ウェブブラウザにおける独自の文字コード判定機能を悪用したクロスサイト・スクリプティング攻撃の対策が未実施である例である。この解決には HTTP レスポンスヘッダの「Content-Type」フィールドに文字コードを指定する必要がある。
投稿フォーム
<textarea name="comment" ... >
<input name="agree" type="checkbox" value="yes">...
<input name="uid" type="hidden" value="12345678">...
確認画面
$comment = escapeHTML(param('comment'));
$agree = param('agree');
$uid = param('uid');
...
print "下記内容で登録します<BR>「 ".$comment."」 ...
print "<input ... hidden ... =\"".$uid ...
上記は、投稿フォームの HTML ソースと、そのフォームから送信された情報のいくつかを確認画面として出力するウェブアプリケーションのソースの一部である。
コメントに対しては、入力値を受け取る段階でエスケープ処理を行っていますが、ユーザID に対してはエスケープ処理を行っていない。これは、エスケープ処理の対象を正しく認識していないために生じる「対策漏れ」の一例である。
【解説】
エスケープ処理対象について、ありがちな誤った認識として、「テキストで入力可能な要素」のみを対象としていることが挙げられる。
攻撃は、フォームのコメント欄のように自由に入力できる項目のみを悪用するわけではない。テキストで入力可能な要素のみに着目すると、その他の要素を見逃すことになる。また、攻撃を入力段階で防御しようとする意識が先行して、入力値を受け取った段階でエスケープ処理を行うことも、対策漏れにつながる。クロスサイト・スクリプティングの脆弱性への対策におけるエスケープ対象は「出力要素」である。
【修正例】「出力」に注目してエスケープ処理
入力要素に注目せず、出力要素に注目してエスケープ処理を実装する。
$comment = param('comment');
$agree = param('agree');
$uid = param('uid');
...
print escapeHTML($comment);...
print "<input ... hidden ... =\"".escapeHTML($uid)."...
誤った対策
入力フォームに入力制限を実装
【脆弱な実装】
<script>
function Check() {
...
}
</script>
...
<input value="確認" onClick="Check();">
JavaScript によるチェック機構を実装し、このチェック機構を介すことにより、確認画面を出力するウェブアプリケーションには許可された入力値のみが渡される。このチェック機構により、確認画面には不正な文字は出力されないと考えがちだが、このチェック機構は、クロスサイト・スクリプティングの脆弱性への対策としては有効に機能しない。
【解説】
対策を実施する箇所が誤っている。入力側(クライアント側)でのチェック機構は、利用者の入力ミスを軽減する目的においては有効に機能するが、クロスサイト・スクリプティングの脆弱性への対策としては有効に機能しない。クロスサイト・スクリプティング攻撃の多くは、悪意ある人が用意した罠(メールのリンクや罠ページ等)から、脆弱なウェブアプリケーションに直接リクエストされるため、本例のような入力側のチェック機構を経由することがないためである。
ブラックリスト方式による入力チェックのみを実装
【脆弱な実装】
if ($a =~ /(script|expression|...)/i){ # 入力チェック
error_html(); # 危険な値を含む場合はエラー表示
exit;
} else {
...
print $a; # 危険な値を含まない場合は処理を先に進める
本例は、ブラックリスト方式による入力チェック機構を実装しているウェブアプリケーションである。ブラックリストには、クロスサイト・スクリプティング攻撃に悪用される危険な文字列を定義している。たとえば、入力値 $a にscript
等を含む場合、処理を先に進めず、エラー画面を表示する。
一見、入力チェック機構が有効に機能し、クロスサイト・スクリプティング攻撃を無効化できるように思われるが、この実装にはチェックを回避され、攻撃が成功してしまう問題が存在する。
【解説】制御文字等を悪用した入力チェックの回避
「入力チェック」は、クロスサイト・スクリプティングの脆弱性への根本的な対策にはならない。たとえば、$a に、下記のような文字列を指定された場合、入力チェックによるscript
のマッチングを回避される。
<s%00cript>alert(0)</s%00cript>
入力チェックを通過した $a は、%00
がデコードされた Null 文字を含む形でウェブページに出力される。ウェブブラウザによってはこの Null 文字を無視するため、結果として $a の出力内容はスクリプト文字列として解釈される。このため、単純なパターンマッチングでは、スクリプトに悪用される文字列を完全に抽出することはできない。Null 文字のような、チェック機構の回避に悪用可能な文字は、他にも複数存在する。
HTTP ヘッダ・インジェクションの例
Perl による URL リダイレクション
【脆弱な実装】
$cgi = new CGI;
$num = $cgi->param('num');
print "Location: http://example.jp/index.cgi?num=$num\n\n";
これは Location ヘッダによって、アクセスしたユーザを指定の URL へリダイレクトさせるプログラムの一部である。この実装では、num パラメータの値が数値であると想定している。
この実装は num パラメータに改行コードを含む値を指定されることを考慮していないため、想定外のHTTP レスポンスを作成されてしまう。
【解説】
この実装では num パラメータに3%0D%0ASet-Cookie:SID=evil
を指定された URL へユーザがアクセスした場合、下記のように攻撃者の意図した Cookie が発行される。また、num パラメータに指定される文字列によっては、ユーザのブラウザへ偽のページを表示されてしまう。
HTTP/1.x 302 Found
Date: Sat, 07 Mar 2009 01:49:48 GMT
Server: Apache/2.2.3 (Unix)
Set-Cookie: SID=evil
Location: http://example.jp/index.cgi?num=3
Content-Length: 292
Connection: close
Content-Type: text/html; charset=iso-8859-1
【修正例】ヘッダに埋め込む文字列に改行コードを許可しない
改行コードを許可しないよう、開発者が適切な処理を実装することで、HTTP ヘッダ・インジェクションの脆弱性に対する根本的解決となる。
### 複数行にわたる文字列から最初の行を返す関数
# 引数: 文字列。 2 つ目以降の引数は無視する
# 戻り値: 改行コード( \r, \n, \r\n)以前の文字列。
sub first_line {
$str = shift;
return ($str =~ /^([^\r\n]*)/)[0];
}
上記は、引数で与えられた文字列の最初の行(改行なし)を返す関数である。外部から入力されるパラメータの値を出力する場合でも、この関数を通すことで、HTTP レスポンスヘッダのフィールド値として適切な形式となり、脆弱性を解消できる。
なお、HTTP ヘッダではフィールド値として複数行にわたる値も許可されているが、この関数は複数行を考慮しない仕様である。複数行にわたる HTTP ヘッダのフィールド値をウェブアプリケーションで扱う場合に、この関数を使用すると、正常なフィールド値を削除してしまうので注意する。
メールヘッダ・インジェクションの例
Perlによるメール送信機能
【脆弱な実装】
open (MAIL, "| /usr/sbin/sendmail -t -i");
print MAIL << "EOF";
To: info\@example.com
From: $email
Subject: お問い合わせ($name)
Content-Type: text/plain; charset="ISO-2022—JP"
$inquiry
EOF
close (MAIL);
これは、ユーザからの問い合わせ内容をウェブサイト運営者にメールで送信するプログラムの一部である。
この実装では、ユーザの入力値をメールヘッダに出力しているため、メールヘッダ・インジェクションの脆弱性がある。
【解説】
この実装では、sendmail コマンドの標準入力にメールヘッダおよびメール本文を与えることでメールを送信する。sendmail コマンドは入力された To ヘッダ、Cc ヘッダ、Bcc ヘッダに基づいて送信先メールアドレスを決定する。ユーザが画面1において「お名前」にanzen
、「メールアドレス」にanzen@example.net
、「お問い合わせ」にHello, World
という値を入力した場合、このプログラムは次のメールを info@example.com に送信する。
To: info@example.com
From: anzen@example.net
Subject: お問い合わせ(anzen)
Content-Type: text/plain; charset="ISO-2022—JP"
Hello, World
しかし、ユーザが「お名前」または「メールアドレス」に改行コードとメールヘッダを含む値を入力すると、任意のメールアドレスにメールを送信できてしまう。例えば、このプログラムにユーザが「メールアドレス」としてanzen@example.net%0d%0aBcc%3a%20user@example.org
を入力した場合、sendmail コマンドへの入力は下記のようになある。sendmail コマンドはこの入力に基づいて、info@example.com、の他に user@example.org にもメールを送信してしまう。
To: info@example.com
From: anzen@example.net
Bcc: user@example.org
Subject: お問い合わせ(anzen)
Content-Type: text/plain; charset="ISO-2022—JP"
Hello, World
【修正例1】ユーザーの入力値をメールヘッダに出力しない
ユーザの入力値をメールヘッダに出力しないことで、メールヘッダ・インジェクションの脆弱性への根本的解決となる。
open (MAIL, "| /usr/sbin/sendmail –t -i");
print MAIL << "EOF";
To: info\@example.com
From: webform\@exmaple.com
Subject: お問い合わせ
Content-Type: text/plain; charset="ISO-2022—JP"
==========================================
お名前: $name
メールアドレス: $email
==========================================
$inquiry
EOF
【修正例2】メールヘッダに展開する変数から改行コードを除去する
メールヘッダに展開する変数から改行コードを除去することで、メールヘッダ・インジェクションの脆弱性への保険的対策となる。
$name =~ s/\r|\n//g;
$email =~ s/\r|\n//g;