PHPでSQLインジェクション攻撃の対策方法【PDO】

PHPでデータベースを扱う際に、絶対に知っておかなければいけないセキュリティ対策として、「SQLインジェクション」というものがあります。

ご存知でない方は、その概要と手口、対策方法について必ず知っておいてください。

SQLインジェクション攻撃とは

SQLインジェクション攻撃とは、意図しないSQLクエリを無理やり実行させ、データベースを操作する攻撃のことです。

SQLインジェクション攻撃を受けるサンプルコード

文章で見てもピンとこないと思いますので、以下のようなログインシステムを組んだと想定してください。

ユーザーID

パスワード

そしてこちらが、POST送信を受け取るPHPのソースコードです。

//SQLクエリを生成
$sql = "SELECT * FROM users WHERE id = '".$_POST['user_id']."' AND password = '".$_POST['user_pass']."'";

//SQLクエリを実行
$data = $pdo -> query($sql) -> fetch(PDO::FETCH_ASSOC);

//空の配列でなければログイン成功
if($data){
    echo 'ログイン成功!';
}

ユーザーIDとパスワードを入力を求め、送信後、データベースに一致するIDとパスワードがあるかをWHERE句でチェックし、ある場合は「ログイン成功」と表示されます。

一見普通な仕組みに見えますが、このクエリの生成方法には大きな問題があります。

例えば、ユーザーIDをこのように入力した場合。

ユーザーID

パスワード

先ほどのフォーム送信を受け取るPHPのソースコードは、ユーザーから送られた$_POSTの内容をそのままSQLクエリに文字列結合していました。

$sql = "SELECT * FROM users WHERE id = '".$_POST['user_id']."' AND password = '".$_POST['user_pass']."'";

結果、SQLクエリはこうなります。

SELECT * FROM users WHERE id = '' OR 1=1;-- 'AND password = ''

idとパスワードが一致していないと本来はデータを取得できないクエリのはずが、「OR 1=1」という絶対に通る条件に書き換えられてしまいました。

しかも末尾に「;–」と付けることで、以降のクエリをコメントアウト(無視)されてしまいます。

これにより、idとパスワードを持たない人でも、別の人のアカウントでログイン出来てしまう乗っ取り行為が行えてしまいます。

このように、本来の意図と異なるSQLクエリを実行されてしまう攻撃を「SQLインジェクション」と呼ぶのです。

今回はあくまでサンプルとして、SQLインジェクションの手口の1つを紹介しましたが、他人のサイトで試すような行為は絶対に控えましょう。
万が一成功した場合、罪に問われる可能性があります。

SQLインジェクション攻撃で想定される被害

先ほどのサンプルでは、ログイン認証を難なく突破できることが出来ました。

しかし、サンプルで示したように、攻撃側は自由にクエリを改ざん出来るので、脆弱性を放置しておくと様々な被害が想定できます。

  • 非公開情報の取得
  • アカウント乗っ取り
  • データの改ざん
  • DELETE句によるデータ破壊

あの有名な「価格.com」も、過去に不正アクセスによりサービスの一時停止を余儀なくされたことがあり、その時の攻撃はSQLインジェクション攻撃ではないかと言われています。

https://tech.nikkeibp.co.jp/it/free/ITPro/Security/20050602/162004/

SQLインジェクション攻撃が出来てしまうサービスは、管理者ではなく利用者にも被害が波及します。

PHPでデータベースを使用し、ユーザーから情報を預かる場合、我々はユーザーの情報を攻撃を守るセキュリティを作る義務があるのです。

PHP(PDO)におけるSQLインジェクション攻撃の対策

PHPでデータベースにアクセスする場合、多くの場合はPDOオブジェクトを使用してクエリを実行するものと思います。

PDOにおける最もポピュラーなSQLインジェクション攻撃の対策方法をお伝えします。

prepareメソッドを使用する

前述の例で挙げたSQLインジェクション攻撃を受けるサンプルコードの悪い点は、ユーザーの送信内容をそのまま結合してクエリにしてしまったことです。

$sql = "SELECT * FROM users WHERE id = '".$_POST['user_id']."' AND password = '".$_POST['user_pass']."'";

悪意のあるユーザーに自由にクエリを書き換えを許してしまうため、ユーザーからの入力内容を文字列結合でクエリにしてはいけません。

これの対策として、prepare()を使用し、構文を事前に確定させましょう。

参考 PDOでprepareの使い方【PHP】

//SQLクエリを生成
$sql = "SELECT * FROM users WHERE id = :id AND password = :password";

//prepareによるクエリの実行準備
$sth = $pdo -> prepare($sql);

//検索クエリの設定
$sth -> bindValue(':id', $_POST['user_id']); //ID
$sth -> bindValue(':password', $_POST['user_pass']); //パスワード

//検索クエリの実行
$sth -> execute();

//結果を配列で取得
$data = $sth -> fetch(PDO::FETCH_ASSOC);

//空の配列でなければログイン成功
if($data){
    echo 'ログイン成功!';
}

prepare()は、事前にSQL文を読み取り、後から値を変更しクエリを実行できる機能です。

今回の場合は、「:id」と「:password」をbindValueで$_POSTの内容に変更し、その後にexecuteでクエリを実行しました。

prepareでクエリを実行する場合、値を自動的にエスケープ処理してくれるため、文字列結合の時のようにクエリを書き換えられることはありません。

  • prepare()でクエリの実行準備
  • bindValue()でクエリ内の値を設定
  • execute()でクエリの実行
  • (SELECT句の場合)fetchまたはfetchAllなどでデータの取得

prepare()はSELECT句のみならず、INSERT、UPDATE、DELETEなどでも使用できます。

そのため、ユーザーから受け取った値をクエリで使用したい場合は、query()ではなくprepare()→execute()でクエリを実行する方式でプログラムを組みましょう。

以上、PHPでSQLインジェクション攻撃の対策方法、でした。

PHP