プリペアド・ステートメント(プレースホルダ)入門

プログラムを書いていると避けて通れないデータベース回りの脆弱性対策。PHP と MySQL を使っていると、落とし穴だらけって感じですね。

PHPはサンプルコードが山ほどWebでも出ていて覚えやすいのですが、動けばOKな安直なサンプルが多いのも困りものです。例えば、次のようなコード。

$db_conf = “host_name”;
$db_name = “test_db”;
$db_user = “test_userid”;
$db_pass = “test_password”;

$userID = 1;

$db_access = mysql_connect($db_conf,$db_user,$db_pass) or die(“MySQLへ接続失敗”);
$sdb = mysql_select_db($db_name,$db_access) or die(“DBの選択失敗”);
mysql_set_charset(‘utf8’);
$sql = “SELECT * FROM table_name where user_id='”.$userID.”‘”;
$result = mysql_query($sql, $db_access) or die(“Error”);
$rows = mysql_num_rows($result);

$showMessage = “<ul>”;
while ($row = mysql_fetch_assoc($result)) {
$nickName = $row[‘nick_name’];
$comment = $row[‘comment’];
$showMessage .= “<li>”.$nickName.” [“.$comment.”]</li>”;
} // end of while
$showMessage .= “</ul>”;

mysql_free_result($result); //メモリ開放
mysql_close($db_access) or die(“切断に失敗”); // 接続を閉じる

echo $showMessage;

普通は「$userID = 1;」の部分をPOSTやGET等で外部からの入力として受け取って処理する事になるので、これだとSQLインジェクションを始めとして、やられ放題になります。

その原因は「$sql = “SELECT * FROM table_name where user_id='”.$userID.”‘”;」と文字列を組み立てたものを「mysql_query」で実行するやり方にある訳です。

こういう最悪のコードサンプルをいまだに初心者に教えるのはどうかと思うです。でも、世の中に溢れているのは、こういう例ばっかりですね。

対策として、フォーム等で受け付ける文字列をサニタイズと称して「エスケープしたり、危険な文字列を削ったり」する手法が紹介されるのですが、何が「危険」になるかはケースバイケースなので、決定打は無し、です(ムキー(笑))。

で、根本対策として、「プリペアド ステートメントとか、プレースホルダとか、バインド機構を使え」と書いてあったりします。IPAの「安全なSQLの呼び出し方」に詳しく書かれているけど、なぜか PHP + MySQL の組み合わせが避けられている。。。

この手法は決して目新しいものでは無いので、色々なサイトでも触れられているのだけど、なぜか概念の説明やコア部分のコードだけで、イマイチ不親切なものが多い。こういうサンプルコードこそ、そのまま動く分かり易いもので初心者に説明すべきだと思うのだけど。なんでだろ?

という訳で、上のコードをプリペアド・ステートメント(静的プレースホルダ)式で書き直すとこうなる(はず)。

$db_conf = “mysql:host=host_name; dbname=test_db;charset=utf8”; // 文字コードは UTF-8 に!
$db_user = “test_userid”;
$db_pass = “test_password”;

try {
// MySQLサーバへ接続
$pdo = new PDO($db_conf, $db_user, $db_pass, array(PDO::ATTR_EMULATE_PREPARES => false));
} catch(PDOException $e){
// エラー処理
var_dump($e->getMessage());
}

// プリペアド ステートメント:準備段階
$sql = “SELECT id, nick_name, comment FROM table_name WHERE user_id = ?”;
$stm = $pdo -> prepare($sql);

// 上のSQLの「?」に値(1)を入れる
$userID = 1;
$stm -> bindValue(1, $userID, PDO::PARAM_INT);

// 実行する
$stm -> execute();
$showMessage = “<ul>”;
while ($row = $stm -> fetch()) {
$showMessage .= “<li>”;
$showMessage .= $row[‘nick_name’];
$showMessage .= ” [“;
$showMessage .= $row[‘comment’];
$showMessage .= ” ]</li>”;
} // end of while
$showMessage .= “</ul>”;

$pdo = null; // オブジェクトを空にする
$stm->closeCursor(); // MySQL接続をクローズする

echo $showMessage;

先にSQLを確定しコンパイルしてしまい(プリペアド・ステートメント)、後から変数部分に値だけを配列に束ねて実行する(バインド機構)ため、危険な文字列の挿入等によって(原理的に)SQL文の破壊が発生しないので、より安全。

また、SQLの中に予め変数が入る場所(プレース)を確保(ホールド)しておくので、「プレースホルダ」とも呼ぶようだ。呼び名が色々で分かりにくい。

なお、「プレースホルダ」には「静的プレースホルダ」と「動的プレースホルダ」があり、前者が正統「プレースホルダ」で、後者は「なんちゃって」なのだそうな。文字列を動的に組み立てるから脆弱性が入り込むので、「動的プレースホルダ」には何らかの付け入るスキがあると思った方が良さそうだ。

上のコード例で

(PDO::ATTR_EMULATE_PREPARES => false)

という部分で、動的プレースホルダ(ATTR_EMULATE_PREPARES)をOFF(false)にしているのは、そういう事なのだそうな。

デフォルトがON(true)なのだけど、それだと裏でSQL文を組み立てて=エミュレート(なんちゃって)処理をしてしまうらしい。何でそんな事をするのか意味不明 (^^;

変数が1個の場合は「?」だけで十分だけど、複数ある場合は「:value_name」を使うとコードが読みやすくなる。

// DB接続までは SELECT と同じ

// プリペアド ステートメント:準備段階
$sql = “INSERT INTO table_name (user_id, nick_name, comment) VALUES (:value1, :value2, :value3)”;
$stm = $pdo -> prepare($sql);

// 各値を代入
$userID = 1;
$nickName = “ほげほげ”;
$comment = “あんなこんなワン”;

$stm -> bindValue(‘:value1’, $userID, PDO::PARAM_INT);
$stm -> bindParam(‘:value2’, $nickName, PDO::PARAM_STR);
$stm -> bindParam(‘:value3’, $comment, PDO::PARAM_STR);

// 実行
$stm->execute();

// 実行結果の行数を数える:「1」と表示されれば成功
$count = $stm->rowCount();
echo $count;

$pdo = null; // オブジェクトを空にする
$stm->closeCursor(); // MySQL接続をクローズする

こんな感じ。

「bindValue」は数値、「bindParam」は文字列を扱う。

「PARAM_INT」「PARAM_STR」も型指定なので、きちんと指定する。


P.S.
当たり前だけど、データベースの接続情報部分は外出しして、Webで公開していない安全なディレクトリに収めたものを include してね。

「プリペアド・ステートメント(プレースホルダ)入門」への2件のフィードバック

    1. Naohiro19さん、コメントありがとうございます。
      長らくコメント頂いたのに気が付かず、ごめんなさい。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です


CAPTCHA