PHP
MySQL

SQLインジェクションとクエリの書き方について考える

今年新卒で弊社に入社してからやっと戦力になってきて、結構大規模なSQLを触ったりするときに指摘されることがあったのでまとめてみました。仕事ではPHP(フレームワーク)とMySQLを使っておりますので、その場合の対策を書き留めたいと思います。

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

SQLインジェクションの脆弱性がある場合、悪意あるリクエストにより、データベースの不正利用をまねく可能性があります。 悪意のある人が入力フォームから不正な値(データベースへの命令文を構成する値)を送信し、データベースが消去されたり改ざんされたりしてしまいます。
(例)
「名前」が「apple」である商品を検索するには、以下のような SQL 文を実行します。

SELECT * FROM items WHERE name = 'apple'

これをアプリケーションで様々な単語で検索したい場合、

$query = 'SELECT * FROM items WHERE name = ' . $name;

となるわけですが、この $name に不正な値を入力して見ます。

$query = 'SELECT * FROM items WHERE name =  'apple'';

などとしてシングルクォートを入れたり、

$query = 'SELECT * FROM items WHERE name = apple; UNION ALL SELECT mail_address AS id, password AS name FROM users';

と入力されてしまうと、文法エラーが起きたり個人(機密)情報の流出の原因となってしまいます。

対策

バインド機構を使う

ユーザー入力を受け取ってSQL文を動的に生成する場合は プレースホルダ と プリペアドステートメントを用いバインドしなければなりません。

  • プレースホルダ
    実際の内容を後から挿入するために、とりあえず仮に確保した場所のことです。

  • プリペアドステートメント
    直訳すると「予約語」と言う意味です。あらかじめ実行するクエリを予約します。この時プレースホルダを用い、クエリにユーザー入力を後から挿入する場所を指定することができます。

$stmt = $pdo->prepare('SELECT * FROM items WHERE name = ? AND price = ?');
$stmt->bindValue(1, $name);
$stmt->bindValue(2, (int)$price, PDO::PARAM_INT);
$stmt->execute();

// または
$stmt = $pdo->prepare('SELECT * FROM items WHERE name = ? AND price = ?');
// 型を明示していないので、$priceはここでは文字列扱いとなります。
$stmt->execute([$name, $price]);

文字列以外のものを扱う際には明示的なキャストが必要になり、省略すると文字列として扱われます。

複文の禁止

PDOのオプションで複文が禁止できるようになったらしいです。 (v5.5.21 および v5.6.5以降)

PDO::MYSQL_ATTR_MULTI_STATEMENTS (integer)
FALSE にすると、PDO::prepare() や PDO::query() でのマルチクエリの実行を無効にします。

この定数が使えるのは、データベースハンドルを新規作成する際の driver_options 配列内だけであることに注意しましょう。

バインド機構が使えない時の対策

バリデーション

フォームから入力された値に対して、徹底的なデータバリデーションを適用するべきです。不正な入力を拒否することによって、ユーザーデータの破壊的な変更の必要性がなくなります。
数字なら数字の、日付なら日付などそれぞれの型に合わせたバリデーションが必要になってきます。PHPのフィルタ拡張を使ったり、各フレームワークの機能を使用してバリデーションを適用してあげましょう。

エスケープ

上記の様な「フォームから取得する型が決まっている時」にバリデーションを適用するのは簡単ですが、自由検索など「フォームの値の型が決まっていない時」は、バリデーションを適用するのは困難です。その場合は、値をエスケープすることでSQLインジェクションの可能性を下げることができます。もしユーザ入力に特殊文字(スラッシュやクォートなど)が入っていた場合にエラーを起こさせないためです。PDO::quotemysqli::real_escape_stringなどを使用してエスケープしましょう。

その他クエリを書く時に気をつけていること

・文字列リテラルを式展開する(単純に見やすい)

PHP
$query = "SELECT * FROM items WHERE name = {$name}";

・標準SQLではテキストはシングルクォートで囲むこと

MySQLではダブルクォートでも動きますが他のRDBMSでは動かないので、シングルクォートで書くほうが無難です。
SELECT * FROM items WHERE name = 'apple'
× SELECT * FROM items WHERE name = "apple"

参考にした資料

安全なSQLの呼び出し方
https://www.ipa.go.jp/files/000017320.pdf

など