5054


PHP と MySQL を利用しています。

以下のようにユーザー入力値など外部からの文字列を使用して SQL クエリーを生成する場合、SQL インジェクションの危険性があります。

[code]

    $dbh = new PDO('mysql:host=127.0.0.1;dbname=test', 'username', 'password');
    
    $email = $_POST['email'];
    $password = $_POST['password'];
    
    // ユーザー入力値を利用して SQL 文を生成
    $sql = "SELECT * FROM users WHERE email = '$email' AND password = '$password'";
    $sth = $dbh->query($sql);
    
    $userInfo = $sth->fetch(PDO::FETCH_ASSOC);
    // ...
    
[/code]

上記のようなコードは SQL インジェクションが行えてしまします。  
例えば `email` に `' OR 1=1 --` が入力されると、以下の SQL
が発行されて期待しない結果（例の場合、他人としてログインできる）になってしまいます。

[code]

    SELECT * FROM users WHERE email = '' OR 1=1 -- ' AND password = ''
    
[/code]

このような SQL インジェクション対策はどのように行うべきでしょうか？


CC BY-SA 3.0
2015-01-22T05:40:16.193
5053
2015-01-24T04:51:06.340
2944
question
php mysql security
PHP での SQL インジェクション対策はどのように行いますか？


# 前提

まず、SQL インジェクション対策を含め、脆弱性対策の前提としてなるべく新しい安定版の PHP を利用するようにしてください。  
バージョンアップにより脆弱性が解消したり、より安全になっています。

なお、以下の方法は PHP 5.3.7 以上で対応しています。（PHP 5.3.6 でも対応していますが、Windowsの実装が不完全です）  
ただし、[PHP 5.3 系は 2014/8/14
でセキュリティサポートが終了](http://php.net/archive/2014.php#id2014-08-14-1)しています。

# PDO + 静的プレースホルダを用いた SQL 文発行

PDO を利用する場合、以下のポイントを押さえることで安全に利用出来ます。

  * 1.文字エンコーディングを指定する
  * 2.静的プレースホルダを用いる
  * 3.バインド時に型を指定する

それぞれについて、以下に実装方法を説明します。

## 1. 文字エンコーディングを指定する

期待した文字エンコーディングとして動作するよう、PDO での DB 接続時に **必ず** 文字エンコーディングを指定します。

[code]

    // DNS に `;charset=utf8` を追加し
    // 文字エンコーディングを utf8 として DB に接続する
    $dbh = new PDO('mysql:host=127.0.0.1;dbname=test;charset=utf8', 'username', 'password');
    
[/code]

[PHP マニュアル: PDO_MYSQL DSN](http://php.net/manual/ja/ref.pdo-
mysql.connection.php)

## 2. 静的プレースホルダを用いる

DB 接続後に、MySQL がネイティブで実装しているプリペアドステートメントを使用するように設定します。

[code]

    // プリペアドステートメントのエミュレーションを無効にして、
    // MySQL ネイティブの静的プレースホルダを使用する
    $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
    
    $sql = "SELECT * FROM users WHERE email = ? AND password = ?";
    $sth = $dbh->prepare($sql); // プリペアドステートメントを準備
    
[/code]

[PHP マニュアル: PDO::setAttribute](http://php.net/manual/ja/pdo.setattribute.php)

## 3. バインド時に型を指定する

プリペアドステートメントを利用するためにパラメータをバインドする時、`PDOStatement::bindParam()`
の第3引数でデータ型を明示的に指定します。これによりデータ型に応じて正しいリテラルが展開されます。

[code]

    // パラメータに文字列としてバインド
    $sth->bindParam(1, $email, PDO::PARAM_STR);
    $sth->bindParam(2, $password, PDO::PARAM_STR);
    
[/code]

[PHP マニュアル:
PDOStatement::bindParam](http://php.net/manual/ja/pdostatement.bindparam.php)

## 安全な PDO の利用例

ここまでの内容をまとめて安全な PDO の利用例を以下に実装例を示します。

[code]

    // 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);
    // ...
    
[/code]

# 文字列連結による SQL 文生成の危険

SQL インジェクション対策を行うために、エスケープを行い期待した通りに SQL
が解釈されるようにすることも可能です。（この方法は推奨できません。理由は後述します）

質問の例の `$email` は文字列リテラルですが、文字列リテラルとして正しく解釈されるように、これを `\' OR 1=1 --`
となるようエスケープ処理を行えば、以下のようなSQLが発行され SQL インジェクションは発生しません。

[code]

    SELECT * FROM users WHERE email = '\' OR 1=1 -- ' AND password = ''
    
[/code]

ただし、独自の SQL インジェクション対策の実装には抜け漏れがある可能性があり、SQL
インジェクションの危険が残ることがあります（そして、危険が残る場合の方が多い）。

SQL インジェクションは多くの専門家が研究しており（脆弱性が指摘されていない、より）安全な方法（前述の PDO +
静的プレースホルダを用いた方法）があるため、データベース管理ツールを作るなどの特別な場合を除いて独自での実装は避けることを推奨します。

# 参考情報

  * [IPA: 別冊：「安全なSQLの呼び出し方」](http://www.ipa.go.jp/security/vuln/websecurity.html)
  * [徳丸浩の日記: 自己流のSQLインジェクション対策は危険](http://blog.tokumaru.org/2013/02/security-measures-of-own-way-are-unsafe.html)
  * [徳丸浩の日記: PDOにおける一応の安全宣言と残る問題点](http://blog.tokumaru.org/2011/08/pdo.html)


CC BY-SA 3.0
2015-01-22T05:40:16.193
5054
2015-01-22T05:58:35.637
2015-01-22T05:58:35.637
2944
2944
5053
answer


上記の延長線上で最も簡単なSQLインジェクション対策方法は、  
bindParamメソッドの引数にmysql_real_escape_string()を仕込むとかいかがでしょうか。

> $sth->bindParam(1, $email, PDO::PARAM_STR);  
>  ↓  
>  $sth->bindParam(1,mysql_real_escape_string($email), PDO::PARAM_STR);


CC BY-SA 3.0
2015-01-24T04:51:06.340
5190
2015-01-24T04:51:06.340
5053
answer