第6章 入力対策
SQL注入: #1 実装における対策
SQL注入対策のうち、プログラミングによる「実装における対策」について以下に述べる。
SQL注入攻撃
SQL注入攻撃は、次のような攻撃である。
ここに、次のようなSQL文を使用したログイン判定プログラムがあるとする。
SELECT uid FROM account_table WHERE uid='ユーザID' AND pw='パスワード'
このSQL文は、ユーザID(uid)とパスワード(pw)を入力パラメータとして受け取り、その組み合わせをデータベーステーブル(account_table)から検索する。
そして、該当するレコードがあった場合に、そのユーザID(uid)を返す処理となっている。このSQL文以降の処理は、ユーザIDが返った場合にのみログインを可能とする内容となっている。
ここで、ユーザIDに「' OR 1=1--」という文字列が与えられた場合は、次のようなSQL文が組み立てられてしまう。
SELECT uid FROM account_table WHERE uid='' OR 1=1--' AND pw='任意の文字列'
実は、与えられた文字列「' OR 1=1--」には、次のような意味がある。
- '
- : ひとつ前の「'」と対となり、文字列定数を終わらせる
- OR 1=1
- : uidの値に関係なく、検索条件を、真とさせる
- --
- : それ以降の内容をコメントとして無視させる
このため、この文字列をパラメータとして与えられた場合は、ユーザIDが常に返ってくるため、本来ログインを許可されていないユーザもログインが可能となってしまうのである。
このように、パラメータを埋め込んでSQL文を組み立てる場合、そのパラメータに特殊記号(記号)を含ませたSQLコマンドを与えることで、データベースの不正操作が可能となる脆弱性、またはその脆弱性に対する攻撃を「SQL注入」という。

SQL注入攻撃の脅威
SQL注入攻撃の脅威は、攻撃者がSQLで利用できる機能を全て操作可能となってしまうことであり、具体的には次の被害が挙げられる。
- 情報の流出
非公開情報(個人情報等)の漏えい等 - 情報の改ざん
データベースに蓄積された情報(商品価格やパスワード等)の改ざん等 - データの破壊
蓄積されたデータの消去やデータベーステーブル自体の破壊等 - サーバの乗っ取り
ストアドプロシージャを利用したサーバの不正操作等
実装面での対策
SQL注入攻撃を迎撃する対策の実装について、この攻撃パターンのデータフローに沿って説明する。まず、入力データの範囲を仕様によって絞り込める場合、
- 入力値チェックの徹底
を行う。次に、SQL文の途中へ入力データの部分を埋め込んで文字列を組み立てるプログラムのロジックにおいて、次のいずれかを行う。
- プリペアドステートメントの使用
- 文脈に応じた特殊記号対策
(1) 入力値チェックの徹底
入力値チェックを徹底することは、SQL注入対策に一定の効果をもつ。入力データの要件について、文字種、桁数、取り得る値のリスト等の範囲を仕様として絞り込める場合、もれなく入力値チェックを行うことは有効である。ただし、その際の入力値チェックはブラウザ上で動作するJavaScriptによるのではなく、Webサーバ側のアプリケーションプログラムによって行う必要がある。
- 例 商品コード:ABT-9800134
- <条件>
-
・固定長の11文字で構成される
・先頭3文字は大文字のアルファベット
・4文字目はハイフン「-」
・残りの7文字は数字
データベースで扱う値に対して上記のような文字種、文字数等の条件を明確にし、ブラウザから渡された値が、入力値として正しい形式であるかどうかをチェックする。条件を細かく設定し、厳密にチェックすることによって、任意のSQL文の混入を避けることができる。
(2) プリペアドステートメントの使用
これは、入力データの部分を埋め込んで文字列を組み立てる際に、文字列連結演算を使用せずに、プリペアドステートメントを使用して、SQL文の組み立てを行う方法である。
プリペアドステートメントは、プレースホルダと値埋め込みAPIから成る解析済みのSQL文であり、コード中のプレースホルダ(予約場所)に入力データを割り当てる機能(バインドメカニズム)である。
次のコードは、プリペアドステートメントの例である。
プリペアドステートメント利用例:<< Java版 >>
String parameter = ユーザが入力した値;
Connection c = データベース接続オブジェクトの取得;
// 値を埋め込む前の形のSQL文をコンパイルし、構文を確定
PreparedStatement st = c.prepareStatement("SELECT name, price FROM product_table WHERE code=?");
st.setString(1, parameter); // ? の場所に値を埋め込む
// クエリの実行
ResultSet rs = st.executeQuery();
[ 参考:http://www.thinkit.co.jp/cert/tech/7/5/3.htm ]
プリペアドステートメント利用例:<< PostgreSQL + PHP版 >>
<?php
String parameter = ユーザが入力した値;
$dbc = pg_connect("dbname=pg_db"); // データベース接続
// 値を埋め込む前の形のSQL文をコンパイルし、構文を確定
$result = pg_prepare($dbc, "query1", 'SELECT name, price FROM product_table WHERE code=$1');
// $1の場所に値を指定してクエリの実行
$result = pg_execute($dbc, "query1", array(parameter));
?>
[ 参考:http://www.phppro.jp/phpmanual/php/function.pg-prepare.html ]
プリペアドステートメントを利用すると、入力データは、数値定数や文字列定数として組み込まれるため、特殊記号が含まれていた場合でも、それはただの文字として扱われることになる。
(3) 文脈に応じた特殊記号対策
これは、SQL文の途中に挿入されると元のSQL文の構文を変化させてしまう特殊記号や文字のはたらきに対して文字ひとつひとつの単位で対処する方法である。
文字列連結演算を用いてSQL文の途中へ値を埋め込む場合、次のふたつの文脈に応じて、それぞれ特殊記号対策を行う。
- '...' の内側:文字列リテラル等として値を埋め込む文脈
- '...' の外側:数値リテラルとして値を埋め込む文脈
1) '...' の内側
値が埋め込まれるのがSQL文の '...' の内側である場合、次の対策を施す。対策の要点は、ユーザの与えた値がSQL文の文字列リテラルの引用符の外側の文法要素を形成しないよう、値に含まれる特殊記号の効力を打ち消すところにある。
シングルクォート「'」のエスケープ
SQL文の組み立てに文字列連結演算を用いる場合、埋め込むパラメータに含まれる特殊記号をエスケープ処理してからSQL文を組み立てる。
エスケープ対象は処理系によって異なるが、一般的には次のとおりである。
' → '' (「'」がふたつ)
このエスケープが可能であるのは、SQL文の文法では、文字列定数中でシングルクォート「'」を2個並べるとそれが1個のシングルクォートそのものを表すという約束になっているからである。
'don''t' '80''s' 'Quark''s Bar' :SQLソースコード上の表記
don't 80's Quark's Bar :文字列の値
バックスラッシュ「\」のエスケープ
\(バックスラッシュ)が使用できるRDBMS(Relational DataBase Management System)の場合、以下のエスケープ処理も必要である。
\ → \\
もし、この対策を行わず「'」を2個にする対策のみ行っていると、次の攻撃が成功してしまう。
\' UNION SELECT uid, pw FROM account_table-- :入力値
SELECT name, price FROM product_table WHERE category='\' UNION SELECT uid, pw FROM account_table--'
これは、2個になったシングルクォート「''」のひとつ目のみが攻撃者の与えた「\」によって中和され、ふたつ目の「'」が特殊記号のはたらきをもったまま残るからである。
2) '...' の外側
値が埋め込まれるのがSQL文の '...' の外側である場合、次の対策を施す。
文字種の厳密な制限
SQLの数値リテラル以外の文法要素を形成するような記号や文字を含む入力値を受理しないようにする。
数値に関する文字種の制約の仕様には、次のものが考えられる。目的に応じて、
これらのいずれか、もしくはこれらに修正を加えたものを用いる。
- 整数──負符号「-」と数字(0?9)
- 小数部をもつ数値──負符号「-」、数字(0?9)、小数点「.」
- 指数部をもつ数値──正負の符号「+」「-」、数字(0?9)、小数点「.」、指数部を示す「E」「e」
入力値に所定のもの以外の文字や記号が含まれている場合、その1項目、もしくは、このときの入力全体を受理しないよう、アプリケーションの動きを制御する。
文字種を制限しないとどうなるか
入力値がSQL文に埋め込まれる箇所が '...' の外側であるということは、悪意あるユーザがより容易に攻撃を成功させる機会を得ることを意味する。例えば、複文を用いた攻撃がある。いくつかのRDBMSではセミコロン「;」で区切って複数のSQL文を記述できるが、それを利用するものが該当する。攻撃者が、単純な整数値の代わりに自分に都合の良いSQL文全体を入力してくる懸念がある。
123; update account_table set pw='foo' where uid='admin' --
さらに、一部のRDBMSでは、セミコロン「;」を書かなくても複文を記述できることが知られている。そのような場合、通常は危険とは見なされない文字と記号のみを用いて、データベースに大きな打撃を与え得る攻撃パターンを記述できてしまう。
123 delete from account_table --
このような状況が生じる懸念があるので、埋め込む値の文字種の制約は厳密なものである必要がある。
対策を損なうもの
(1) プリペアドステートメントの効果を損なうもの
上記の(2)のように プリペアドステートメントを利用した場合でも、データベースによっては、バインドメカニズムを使用せず、API内部で特殊記号のエスケープ処理と文字連結を行っているのみの場合もある。
例えば、Oracleのようにデータベースエンジンそのものがバインドメカニズムをもっている場合は、データベースエンジン側にプレースホルダ用メモリ領域が用意され、そこにバインド変数値が直接渡される。組み立てたSQL文として渡されるのではなく、SQL文の要素となる値として渡される。したがってバインド変数の値がSQL文として機能することはない。
一方、MySQL4.x以前のようなバインドメカニズムをもたないデータベースエンジンの場合、DBIインタフェース等のデータベースライブラリが内部的にバインド変数をエスケープしてSQL文を組み立て、そのSQL文をデータベースエンジンへ渡している。
なお、MySQL5.0からはバインドメカニズムがサポートされている。
[ 参考:http://dev.mysql.com/doc/refman/5.0/en/sqlps.html ]
ただし、PEAR(PHP Extension and Application Repository)を使ってDBアクセスを行い、PEARでのプリペアドステートメントを利用している場合は、注意が必要である。
PEARは、コミュニティーにより運営されるプロジェクトのことで、コード配布およびパッケージ管理のためのシステムやPHPのコード作成に関する標準スタイル、PHP拡張モジュール・コミュニティライブラリ等を提供することを目的にしている。
ここでは、PEARをPHPの拡張モジュールとして記述している。
このPEARのプリペアドステートメントは内部的にバインド変数をエスケープしてSQL文を組み立るため、有効に機能していない。
このように、利用するデータベースにより、プリペアドステートメントの仕様が異なるので、マニュアル等で仕様を確認すべきである。
この対策は、利用するデータベース処理系が反応する特殊文字のパターンに厳密に合わせ、「実装面での対策」の(1)入力値のチェックや(2)特殊記号のエスケープを確実に行うことである。
(2) 特殊記号対策の効果を損なうもの
実装面での対策で上記の「(3) 文脈に応じた特殊記号対策」を行っても、文字コードに注意を払わないとエスケープ対策漏れになってしまう。以下に示すようなマルチバイト文字は特に注意が必要である。
- マルチバイト文字には、「\」と同じ文字コード(0x5c)をもつ文字が存在する
(例えば、「表」(0x95 0x5c)等)
そのため、シフトJISに対応しないエスケープ処理関数を用いると意図しないエスケープ結果となってしまう場合があり、これを悪用してSQL注入を成立させることも可能である。
この対策は、次のとおりである。:
- 対応する文字コードへの変換
文字列をシフトJIS文字列のまま扱わずに、エスケープ処理関数が対応する文字コードへ変換してから使用する。 - 文字列に不正な文字コードが無いかをチェックする
同じ文字コードへのコード変換をパラメータをに対して行い、変換前と変換後のパラメータが異なっている場合は、パラメータに文字コードが含まれると判断する。