2013年2月23日土曜日

自己流のSQLインジェクション対策は危険

SQLインジェクションについて解説したブログ記事を読んだところ、ユニークな対策方法が紹介されていました。この対策方法の問題点について解説し、正しい脆弱性対処の重要性を説明します。

ユニークなSQLインジェクション対策


SQLインジェクションのブログ記事を読んでいて、対策として以下のスクリプトが紹介されていました。
$_GET[id]=strip_tags(htmlspecialchars($_GET[id]));
$_GET[id]=preg_replace(array(‘/[~;\'\"]/’,'/–/’),”,$_GET[id]);
現在は当該のスクリプトは削除されているので、当該エントリは参照せず、「昔々あるところにこのようなSQLインジェクション脆弱性対処スクリプトがあった」という想定で以下の議論を進めます。

問題点の説明

このスクリプトはいくつかの問題点があります。
  • $_GETを直接変更していて汎用性が乏しい
  • idをクォートしていないなどいくつかの文法エラー
  • 表示前提でないのにHTMLエスケープしている
  • HTMLエスケープした後strip_tagsを呼んでいるが、この段階ではタグはエスケープ済みなので、strip_tagsは何もしない
  • 「危険な文字」を削除するサニタイジング手法を用いている
このままでは動かすことができないので、以下のようにsanitize関数として少し修正したものを紹介します。
function sanitize($str) {
  $str = strip_tags(htmlspecialchars($str));
  return preg_replace(array('/[~;\'\"]/', '/-/'), '', $str);
}
この関数に、さまざまな文字を通した結果を下表に紹介します。

入力文字htmlspecialcharsstrip_tagspreg_replace(最終)
AAAA
5555
<&lt;&lt;&lt
>&gt;&gt;&gt
&&amp;&amp;&amp
"&quot;&quot;&quot
'''(空)
\\\\
;;;(空)
---(空)
~~~(空)
####

HTMLエスケープの対象となる < > & " の4文字は、文字実体参照に変換された後、preg_replace関数でセミコロンを削除してしまうので、中途半端な妙な文字化けになりそうです。
一般的な原則としては、データベースにはHTMLの形ではなくプレーンテキストの形で保存しておき、HTMLとして表示する直前にHTMLエスケープする方法で統一することで、上記のような文字化けやエスケープ漏れをなくすことがよいでしょう。

脆弱性はないのか

このsanitize関数に脆弱性はないでしょうか。上表のように、バックスラッシュ(円記号)を素通ししているので、MySQLや、設定によってはPostgreSQLの場合に、問題が生じそうです。以下、それを説明します。以下の説明では、MySQLを使う想定とします。
以下のように、ログイン処理を想定したSQL文組立があったとします。
$sql = sprintf("SELECT * FROM users WHERE id='%s' AND pwd='%s'", sanitize($id), sanitize($pwd));
ここで、$idと$pwdが以下だったとします。
$id = '\\';   // \ 1文字
$pwd = 'password';
$sqlは下記となります。
SELECT * FROM users WHERE id='\' AND pwd='password'
ここで、id=の後の文字列リテラルはどこまででしょうか。\'は「シングルクォートを\でエスケープしたもの」、すなわちリテラル内の文字と解釈されるので、その次のシングルクォートまでが文字列リテラルになります。つまり、 AND pwd=まで(黄色でマークした部分)が文字列リテラルと解釈されます。その後の password' は「文字列リテラルの外」、すなわちSQL文の構文と解釈されますが、SQL文として正しくないので、このSQL文は構文エラーになります。
それでは、passwordのところをSQL文として正しくなるように変更したらどうでしょうか。これがSQLインジェクション攻撃です。その例を以下に示します。
$id = '\\';   // \ 1文字
$pwd = ' OR 1=1#';
組み立てられたSQL文は以下となります。
SELECT * FROM users WHERE id='\' AND pwd=' OR 1=1#'
passwordの代わりに、OR 1=1(青くマークした部分)が正しいSQL文(の一部)となります。#以降はMySQLのコメントとして無視されます。すなわち、これで認証回避が可能となりました。

「自己流の脆弱性対策」の危険性

以上に説明したように、htmlspecialchars、strip_tags、preg_replaceの三種類の関数を使った「独自の」でSQLインジェクション対策には抜けがあり、SQLインジェクション攻撃を許してしまう可能性があることがわかりました。
SQLインジェクションを含めて、脆弱性対処の方法論は、世界中の専門家がさまざまな観点から研究しています。SQLインジェクション対策に関しては、数年前から「究極形」がわかっており、特別な事情のある場合を除いて、わざわざ別の方法を用いる必要はないでしょう。特別な場合とは以下の様なケースです。
  • データベースアクセスを含むフレームワークを新たに開発する場合
  • O/Rマッパーを自作する場合
  • phpMyAdminのようなデータベース管理ツールを自作する場合
大半の開発者は、上記を作らないでしょう。だから、以下に示す、SQLインジェクション対策の「究極形」に従うことを強く推奨します。それは下記のとおりです。
  • 文字列連結(sprintf等を含む)を用いてSQL文を組み立てない
  • 静的プレースホルダを用いてSQL文にパラメータを割り当てる
  • データベース接続時に文字エンコーディングを指定する
上記の理論的な根拠については、IPAの「安全なSQLの呼び出し方」を参照下さい。また、PHP+PDOを用いた実際的なSQLアクセスの方法については、「PDOにおける一応の安全宣言と残る問題点」を参照下さい。
また、SQLインジェクション対策の解説をこれから書こうとする人には、ぜひajiyoshiさんの素晴らしいブログ記事「Webアプリケーションとかの入門本みたいのを書く人への心からのお願い。」をお読み下さい。このような文章が、現場の開発者から出てくることは本当に素晴らしいことです。

まとめ

自己流の脆弱性対処の危険性について、具体例を用いて説明しました。SQLインジェクションは、非常に危険な脆弱性ですが、完全な対処の仕方が分かっていて、正しいプログラミングをする限り恐れる必要はありません。ここで紹介した解説資料を参照して、あなたのWebサイトを安全にしてください。



4 件のコメント:

  1. こまかいtypoですが。。

    「自己流の脆弱性対策」の危険性 の2行目あたり
    s/わかりしまた/わかりました/

    返信削除
  2. ご指摘ありがとうございます。修正しました。

    返信削除
  3. PostgreSQLの場合に…と前振りがあるのに「MySQLのコメントとして無視されます」になっています。

    返信削除
  4. コメントありがとうございます。MySQLの場合と、PostgreSQLの場合に問題が生じる「可能性」がありますが、実証はMySQLについて行ったという意味です。PostgreSQLの場合、#がコメントの開始とみなされず、--から始まる文字列あるいは /* で始まり */ で終わる形式のみ許されるので、攻撃は難しいかもしれませんが、絶対大丈夫とも言えないと考えます。

    返信削除

フォロワー