2011年8月22日月曜日

PDOにおける一応の安全宣言と残る問題点

 8月18日にPHP5.3.7がリリースされました。このリリースにより、PDOのSQLインジェクションの問題が一応解決されたと判断しましたので、ここに「一応の安全宣言」を表明するとともに、残る問題について報告します。

PDOの問題とは何か

 以前、ぼくがPDOを採用しなかったわけ(Shift_JISによるSQLインジェクション)にて報告したように、PHP5.3.5以前のPDOにはDB接続時に文字エンコーディングを指定する機能がないため、文字列リテラルのエスケープの際に文字エンコーディングをLatin1を仮定してしまうという問題がありました。この状態ですと、DBにShift_JISで接続している際に、SQLインジェクション脆弱性が混入しました。

※ 実は、先のエントリの「追記(2010/07/01 22:20)」に紹介した方法で文字エンコーディングを指定できるのですが、ほとんど知られていないのと、Windowsでは使えないという問題がありました。

 このSQLインジェクションは、プレースホルダを用いてSQL呼び出ししている場合にも起きえます。以下の条件がすべて満たされる場合です。

  • MySQLを用いている
  • 接続時に文字エンコーディングを指定していない
  • MySQLとの接続の際の文字エンコーディングとしてShift_JISを使っている
  • 動的プレースホルダ(MySQLのデフォルト)

 こんなサイトがあるのかと思われる方もおられるでしょうが、私は意外にあると予想しています。この種のサイトをソースコード診断で見つけたこともありますし、去年のPHPカンファレンスの講師飲み会で「MySQLにShift_JISで接続しているサイトありますかね」と周囲の方に聞いてみたところ、ケータイサイトはShift_JISだし、文字コード変換したくない人はShift_JISで接続している場合もあるのではないかという答えでした。

PHP5.3.6での改善

 この状況がPHP5.3.6(3月17日リリース)で改善されます。PDOにて、DB接続時に文字エンコーディングが指定できるようになったのです。しかし、さっそく検証した結果、以下の状態で完全にはSQLインジェクションが解消されてはいませんでした。
  • Linux版ではOK
  • Windows版ではShift_JIS時のエスケープが不完全

 このときの内容は、エントリ「PHP5.3.6からPDOの文字エンコーディング指定が可能となったがWindows版では不具合(脆弱性)あり」にまとめました。
 Windows版での不具合原因は、千葉征弘さん(@nihen)さんが調査した結果、単純なバグだと分かりました。その内容は、PDO/MySQL(Windows版)の文字エンコーディング指定の不具合原因にまとめました。

PHP5.3.7での改善

 千葉さんがレポートを書いてくださった結果、Windows版での不具合は、PHP5.3.7にて修正されました。

今後の推奨する書き方

 今後は、PDOを使う場合は以下のようにするとよいでしょう。
<?php
  $dbh = new PDO('mysql:host=DBHOST;dbname=test;charset=utf8', USERNAME, PASSWORD);
  $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);  // 静的プレースホルダを指定

  $sth = $dbh->prepare("select * from test WHERE a=? and c=?");
  $sth->setFetchMode(PDO::FETCH_NUM);
  $a = 'abc';
  $c = 1;
  $sth->bindParam(1, $a, PDO::PARAM_STR);
  $sth->bindParam(2, $c, PDO::PARAM_INT);
  $sth->execute();
  while ($data = $sth->fetch()) {
    var_dump($data);
  }

 new PDOの行で、文字エンコーディングをchrset=utf8と指定しています。PHP5.3.7以降、Shift_JISを用いてもSQLインジェクションは起きなくなりましたが、utf8を指定するのが色々な意味で安全です。
 次の行では、静的プレースホルダを指定しています。静的プレースホルダを使えば、SQLに指定するパラメータ操作によるSQLインジェクション攻撃は原理的にできなくなりますので、安全性が高まります。
 プレースホルダの値指定(バインド)は、execute関数で配列に指定する方法もありますが、上記のようにbindParam(あるいはbindValue)を用い、型を指定するほうがよいでしょう。型を指定しないと文字列型が仮定されますが、SQLの暗黙の型変換はワナがいっぱいで指摘したような問題が起きるため、型は明示すべきです。

 型を明示する場合としない場合の違いを、動的プレースホルダの場合に生成されるSQLを例示して説明します。まず、型を明示しない場合の呼び出し例をしめします。列aは文字列、列cは整数型とします。
$sth = $dbh->prepare("select * from test WHERE a=? and c=?");
$sth->execute(array('abc', 1));
 この場合のSQL呼び出しをパケットキャプチャを用いて示します。

 ご覧のように、列cに対して、c='1'と文字列リテラルを用いて条件式が展開されています。次に、型を明示した場合です。
$sth = $dbh->prepare("select * from test WHERE a=? and c=?");
$sth->bindValue(1, 'abc', PDO::PARAM_STR);
$sth->bindValue(2, 1, PDO::PARAM_INT);
$sth->execute();
 この場合のSQLは以下のようになります。

 このように、型を明示すると、整数型のパラメータは数値リテラルとして展開されますが、型を明示しないと、文字列リテラル'1'として展開されます。

quoteメソッドの問題

 PDOにはquoteメソッドというものがあり、様々な型のリテラルのエスケープとクオートを自動的に行います。便利なメソッドですが問題が二つありました。

  • quoteメソッドでも文字エンコーディングを考慮しておらず、Shift_JISを用いている場合にSQLインジェクション脆弱性が混入する(PHP5.3.6以前)
  • 数値の場合の処理がおかしい(脆弱性ではない。参照→quoteメソッドの数値データ対応を検証する

 これらのうち、文字エンコーディングの考慮についてはPHP5.3.7で修正されました。
 数値の場合の問題は依然として直っておらず、文字列として処理されます。静的プレースホルダを用いる場合はquoteメソッドを使う必要はありませんが、既存システムの脆弱性対策等でやむを得ずプレースホルダを使用できない場合は、数値に関してはキャストするか、バリデーションで対応することになります。

まとめ

 最近のPDOの安全性向上と残る課題について報告しました。
 PDOを使う際の安全上のポイントは以下の通りです。

  • 接続時に文字エンコーディング(UTF-8を推奨)を必ず指定する
  • 静的プレースホルダを用いてSQLを呼び出す
  • バインドの際に型を明示する

 残る課題としては、quoteメソッドの数値型の処理が依然としておかしいことです。quoteメソッドは数値に対して用いないことで対処することになりますが、プレースホルダを使えば、そもそもquoteメソッドの出番はないので、プレースホルダの使用を強く推奨します。

0 件のコメント:

コメントを投稿

フォロワー