概要
SQL インジェクションは、プレースホルダを利用することで対策が可能ですが、Like 句のワイルドカードついては、ほとんどの情報元が「別途エスケープが必要です」としか記載されていません。
この記事では、MySQL に対するワイルドカードのエスケープ方法の、PHP + PDO + MySQL と PHP + MDB2 + MySQL の2種類の実装を例示します。
PHP の 5.4系、5.5系は今後 PDO の採用を推奨します。5.3系では、状況に合わせて適切な方を選択してください。
PDO と MDB2 の詳細については、以下の記事を参照してください。
サンプルアプリケーションの概要は、以下の通りです。
MySQL のワイルドカードのエスケープ
MySQL のワイルドカードは、以下の 2 つになります。
% | 0個以上の文字 |
_ | 1文字 |
MySQL :: MySQL 5.1 リファレンスマニュアル :: 11.3.1 文字列比較関数
MySQL では、以下の SQL 文のように ESCAPE 句がない場合は、「\」がエスケープ文字と仮定されます。しかし、SQL の規格(ISO および JIS)では ESCAPE 句がない場合はエスケープ文字が定義されないので、ESCAPE 句は必ず書くことを推奨します(「体系的に学ぶ 安全なWebアプリケーションの作り方」より)。
WHERE NAME LIKE '%#%%' ESCAPE '#'
PDO 版
・input.php
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> </head> <body> <h1>名前検索</h1> <form action="search.php" method="post"> 名前:<input type="text" name="name"> <input type="submit" value="検索"> </form> </body> </html>
・search.php
<?php define("USERNAME", "user01"); define("PASSWORD", "pass"); session_start(); $name = $_POST["name"]; try{ //文字エンコーディングを必ず指定する $dbh = new PDO("mysql:host=192.168.11.8;dbname=Test;charset=utf8", USERNAME, PASSWORD); // 静的プレースホルダを指定 $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); //プレースホルダで SQL 作成 $sql = "SELECT ID,NAME FROM USERS WHERE NAME LIKE ? ESCAPE '#';"; $stmt = $dbh->prepare($sql); $stmt->setFetchMode(PDO::FETCH_ASSOC); //パラメーターをエスケープしてバインド $stmt->bindParam(1, escapeString($name), PDO::PARAM_INT); $stmt->execute(); //結果格納配列 $result = array(); //検索結果格納 while ($row = $stmt->fetch()) { $result += array($row['ID'] => $row['NAME']); } $stmt = null; } catch(PDOException $e){ echo $e->getMessage(); } if (count($result) > 0) { //検索結果をセッションに格納 $_SESSION['result'] = $result; //リダイレクト header("HTTP/1.1 301 Moved Permanently"); header("Location: result.php"); } else { //リダイレクト header("HTTP/1.1 301 Moved Permanently"); header("Location: input.php"); } function escapeString($s) { //ワイルドカードをエスケープ return "%" . mb_ereg_replace('([_%#])', '#\1', $s) . "%"; } ?>
・result.php
<?php session_start(); $result = $_SESSION['result']; ?> <!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> </head> <body> <h1>検索結果</h1> <?php //検索結果出力 foreach ($result as $id => $name) { echo htmlspecialchars($id . ":" . $name, ENT_QUOTES, "UTF-8") . "<br/>"; } //セッション変数クリア unset($_SESSION['result']); ?> </body> </html>
MDB2 版
・input.php
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> </head> <body> <h1>名前検索</h1> <form action="search.php" method="post"> 名前:<input type="text" name="name"> <input type="submit" value="検索"> </form> </body> </html>
・search.php
<?php require_once 'MDB2.php'; session_start(); $name = $_POST["name"]; //DB接続 $db = MDB2::connect("mysql://user01:pass@localhost/Test?charset=utf8"); if (MDB2::isError($db)) { throw new exception("DB connection error."); } //プレースホルダで SQL 作成 $sql = "SELECT ID,NAME FROM USERS WHERE NAME LIKE ? ESCAPE '#';"; //パラメーターの型を指定 $stmt = $db->prepare($sql, array('text')); //パラメーターをエスケープして SQL 実行 $rs = $stmt->execute(array(escapeString($name))); //結果格納配列 $result = array(); //検索結果格納 while ($row = $rs->fetchRow(MDB2_FETCHMODE_ASSOC)) { $result += array($row['id'] => $row['name']); } //DB接続切断 $db->disconnect(); if (count($result) > 0) { //検索結果をセッションに格納 $_SESSION['result'] = $result; //リダイレクト header("HTTP/1.1 301 Moved Permanently"); header("Location: result.php"); } else { //リダイレクト header("HTTP/1.1 301 Moved Permanently"); header("Location: input.php"); } function escapeString($s) { //ワイルドカードをエスケープ return "%" . mb_ereg_replace('([_%#])', '#\1', $s) . "%"; } ?>
・result.php
<?php session_start(); $result = $_SESSION['result']; ?> <!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> </head> <body> <h1>検索結果</h1> <?php //検索結果出力 foreach ($result as $id => $name) { echo htmlspecialchars($id . ":" . $name, ENT_QUOTES, "UTF-8") . "<br/>"; } //セッション変数クリア unset($_SESSION['result']); ?> </body> </html>
参考
Webアプリケーションセキュリティに関する記事は、以下のページにまとまっています。ぜひご確認ください。
コメント