俺の脳内選択肢が、SQLインジェクション対策を全力で邪魔している

PHP Advent Calendar 2013 in Adventarの19日目です。昨日も私の「PDOでの数値列の扱いにはワナがいっぱい(2)」でした。

うっかりtogetterなんか見てしまい、無駄に時間を使ってしまったと後悔した上に混乱してしまい余計にわからなくなってしまった人もいるかも知れません。

そこで、せっかくの機会なので、SQLインジェクション対策について、現在の私の考えをまとめておこうと思います。

選べ
①SQLインジェクション対策にプリペアドステートメントを使う
②SQLインジェクション対策にエスケープを使う

もし、上記のような選択にはまってしまったら、あなたのSQLインジェクション対策は、現実的には、ほぼ100%間違っていると言えるのではないでしょうか。プリペアドステートメントとエスケープは、このような対立構造にはありませんから。

なお、この記事は、SQLインジェクション対策をどう教えるかという教育的な視点ではなく、どう実際にコードを書くかという実務的な視点からの、現在の個人的なまとめです。

SQLインジェクションとは?

ユーザからの文字列をそのまま使用してSQL文を作成することにより、プログラマが意図しないSQL文が実行される脆弱性です。

データベースにあるデータが漏洩、改竄、破壊される可能性がある大変危険な脆弱性です。

典型的なSQLインジェクション可能なサンプルは以下のようなコードです。

$gid  = $_GET['gid'];
$sort = $_GET['sort'];
$sql = "SELECT * FROM users WHERE gid=$gid ORDER BY $sort";
$db->query($sql);

上記では、$gid$sortはユーザが自由に値を操作できるので、どんな値でも入力できます。その値をそのまま使ってSQL文を作成するため、例えば、$gidに「1」を$sortに「; delete from users」を入力すれば、実行されるSQL文は以下のようになります。

SELECT * FROM users WHERE gid=1 ORDER BY name; delete from users

複文が実行できる環境(PostgreSQLやPDO+MySQLで動的プレースホルダの場合)では、これでusersテーブルの全データを削除することが可能です。

なお、複文が実行できない環境の場合は、複文が実行できる環境よりは攻撃の自由度が下がりますが、SQLインジェクションは依然として可能です。

SQLインジェクション対策

実際のSQLインジェクション対策のルールは主に次の2つです。

  • (1) リテラル部分ではプレースホルダを使用する
  • (2) リテラル以外の部分はホワイトリストで検証する

順に説明します。

SQL文の構造

SELECT id,name FROM users WHERE gid >= 10 AND city = '名古屋' ORDER BY name

上記のSQLの場合、SQL文の構造は以下のようになります。

名称
キーワード(予約語) SELECT FROM WHERE AND ORDER BY
演算子など , >= =
識別子 id name users gid city
リテラル 10 '名古屋'

(1) リテラル部分ではプレースホルダを使用する

リテラル部分は、プリペアドステートメントでパラメータ化できるため、プリペアドステートメントを使います。

以下のサンプルでは:gidがプレースホルダです。

$gid  = $_GET['gid'];
$sort = $_GET['sort'];

// SQL文を準備
$sql = "SELECT * FROM users WHERE gid=:gid ORDER BY $sort";
$prepare = $db->prepare($sql);

// 準備されたSQL文の「:gid」の部分に$gidの値をバインド
$prepare->bindValue(':gid', (int) $gid, PDO::PARAM_INT);

// SQL文を実行
$prepare->execute();

ただし、上記ではまだ識別子であるカラム名の部分($sort)でSQLインジェクションが可能です。

(2) リテラル以外の部分はホワイトリストで検証する

リテラル以外の部分は、プリペアドステートメントでパラメータ化できないので、動的にSQL文を作成するか、あらかじめ全パターンのSQL文を用意しておくしかありません。

ここで、あらかじめ全パターンのSQL文を用意しておくというのは、動的に検索条件が増えたりIN句を使う場合などパターン数があまりにも多くなってしまう場合は、実用的ではありません。

通常のアプリでは、テーブル名、カラム名などの識別子はあらかじめ決まっています。

ですから、SQL文を作成する際、プログラマがあらかじめ定義した値のみが使われるように(ユーザからの入力がそのまま使われることがないように)、ホワイトリストで検証します。

$gid  = $_GET['gid'];
$sort = $_GET['sort'];

// ホワイトリスト
$sort_whitelist = array(1 => 'id', 2 => 'name');
// ホワイトリストによる検証
$sort_safe = isset($sort_whitelist[$sort]) ? $sort_whitelist[$sort] : $sort_whitelist[1];

$sql = "SELECT * FROM users WHERE gid=:gid ORDER BY $sort_safe";

キーワードや演算子もSQLで決まっていますので同様です。また、キーワードを動的に変化させる必要があるケースはORDER BYのASC、DESCくらいではないかと思います。

なお、上記のようなコーディングでは、識別子をエスケープ(というかクォート?)していないため、キーワードと同じ識別子(例えば、カラム名group)がSQLエラーになり使えません。

例外的なケース

通常は、上記の

  • (1) リテラル部分ではプレースホルダを使用する
  • (2) リテラル以外の部分はホワイトリストで検証する

の2つでSQLインジェクションを完全に防ぐことができますが、例外的なケースではそれらの対策が使えないことがあります。

プリペアドステートメントが使えない場合

プリペアドステートメントが使えない環境では、リテラルを正しくエスケープする必要があります。

リテラルのエスケープ方法については、http://www.ipa.go.jp/security/vuln/websecurity.htmlの「安全なSQLの呼び出し方」を参照してください。

識別子をユーザが指定する場合

テーブルを作成したり、変更したりするphpMyAdminのようなのデータベース管理ツールを作成する場合は、ホワイトリストが使えません。

ホワイトリストが使えない場合は、識別子を正しくエスケープする必要があります。

識別子のエスケープ方法については、

を参照してください。

この記事で扱っていないこと

  • 文字エンコーディングの正しい指定方法
    • 文字エンコーディングが正しく指定されていないとSQLインジェクションが可能な場合がある
  • 動的プレースホルダと静的プレースホルダの違い
    • 実装にバグがなければどちらもSQLインジェクション対策としては有効
    • 原理的には静的プレースホルダがより安全
  • LIKE句でのワイルドカードのエスケープ処理
    • SQLインジェクションの原因にはならないがDoSになる可能性はある
  • 個別の実装に関するワナとしか思えないような仕様など
    • PDOでのワナなど
    • たぶんSQLインジェクションの原因になるものはない

まとめ

SQLインジェクションの原因は、SQL文の作成時にユーザに由来する入力値をそのまま使ってしまうことです。

通常は、以下の対策でほぼ100%対応可能だと思います。

  • (1) リテラル部分ではプレースホルダを使用する
  • (2) リテラル以外の部分はホワイトリストで検証する

参考

明日は、ngyukiさんの「Visual Studio Express 2013 で PHP をステップ実行する」です。

Date: 2013/12/19

Tags: security, pdo, sql, database