質問: '' ``` このような SQL インジェクション対策はどのように行うべきでしょうか?
回答: 前提 == まず、SQL インジェクション対策を含め、脆弱性対策の前提としてなるべく新しい安定版の PHP を利用するようにしてください。 バージョンアップにより脆弱性が解消したり、より安全になっています。 なお、以下の方法は PHP 5.3.7 以上で対応しています。(PHP 5.3.6 でも対応していますが、Windowsの実装が不完全です) ただし、PHP 5.3 系は 2014/8/14 でセキュリティサポートが終了しています。 PDO + 静的プレースホルダを用いた SQL 文発行 =========================== PDO を利用する場合、以下のポイントを押さえることで安全に利用出来ます。 * 1.文字エンコーディングを指定する * 2.静的プレースホルダを用いる * 3.バインド時に型を指定する それぞれについて、以下に実装方法を説明します。 1. 文字エンコーディングを指定する 期待した文字エンコーディングとして動作するよう、PDO での DB 接続時に**必ず**文字エンコーディングを指定します。 ``` // DNS に `;charset=utf8` を追加し // 文字エンコーディングを utf8 として DB に接続する $dbh = new PDO('mysql:host=127.0.0.1;dbname=test;charset=utf8', 'username', 'password'); ``` PHP マニュアル: PDO_MYSQL DSN 2. 静的プレースホルダを用いる DB 接続後に、MySQL がネイティブで実装しているプリペアドステートメントを使用するように設定します。 ``` // プリペアドステートメントのエミュレーションを無効にして、 // MySQL ネイティブの静的プレースホルダを使用する $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); $sql = "SELECT * FROM users WHERE email = ? AND password = ?"; $sth = $dbh->prepare($sql); // プリペアドステートメントを準備 ``` PHP マニュアル: PDO::setAttribute 3. バインド時に型を指定する プリペアドステートメントを利用するためにパラメータをバインドする時、`PDOStatement::bindParam()` の第3引数でデータ型を明示的に指定します。これによりデータ型に応じて正しいリテラルが展開されます。 ``` // パラメータに文字列としてバインド $sth->bindParam(1, $email, PDO::PARAM_STR); $sth->bindParam(2, $password, PDO::PARAM_STR); ``` PHP マニュアル: PDOStatement::bindParam 安全な PDO の利用例 ここまでの内容をまとめて安全な PDO の利用例を以下に実装例を示します。 ``` // 1. 文字エンコーディングを指定して DB に接続する $dbh = new PDO('mysql:host=127.0.0.1;dbname=test;charset=utf8', 'username', 'password'); // 2-1. 静的プレースホルダを用いるようにエミュレーションを無効化 $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); // エラー時に例外を発生させる(任意) $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $sql = "SELECT * FROM users WHERE email = ? AND password = ?"; // 2-2. プリペアドステートメントを準備 $sth = $dbh->prepare($sql); // 3. 型を指定してパラメータにバインドする $sth->bindParam(1, $email, PDO::PARAM_STR); $sth->bindParam(2, $password, PDO::PARAM_STR); // SQL の実行 $sth->execute(); $userInfo = $sth->fetch(PDO::FETCH_ASSOC); // ... ``` 文字列連結による SQL 文生成の危険 =================== SQL インジェクション対策を行うために、エスケープを行い期待した通りに SQL が解釈されるようにすることも可能です。(この方法は推奨できません。理由は後述します) 質問の例の `$email` は文字列リテラルですが、文字列リテラルとして正しく解釈されるように、これを `\' OR 1=1 --` となるようエスケープ処理を行えば、以下のようなSQLが発行され SQL インジェクションは発生しません。 ``` SELECT * FROM users WHERE email = '\' OR 1=1 -- ' AND password = '' ``` ただし、独自の SQL インジェクション対策の実装には抜け漏れがある可能性があり、SQL インジェクションの危険が残ることがあります(そして、危険が残る場合の方が多い)。 SQL インジェクションは多くの専門家が研究しており(脆弱性が指摘されていない、より)安全な方法(前述の PDO + 静的プレースホルダを用いた方法)があるため、データベース管理ツールを作るなどの特別な場合を除いて独自での実装は避けることを推奨します。 参考情報 ==== * IPA: 別冊:「安全なSQLの呼び出し方」 * 徳丸浩の日記: 自己流のSQLインジェクション対策は危険 * 徳丸浩の日記: PDOにおける一応の安全宣言と残る問題点