[PHP] WEBアプリケーションの脆弱性 - SQLインジェクション脆弱性と対策について

WEBアプリケーションの脆弱性の一つである、SQLインジェクションについて、その手口と対策をまとめていきます。

SQLインジェクションは、クロスサイトスクリプティングと並んで、WEBアプリケーションにおける脆弱性の代表格とされており、近年でもこの脆弱性を利用した攻撃による情報漏洩が発生しています。

しかし、アプリケーション、またはインフラストラクチャーで対策することにより確実に防げるので、その仕組みと対策をしっかりと理解しておくことが大切です。

なお、クロスサイトスクリプティングについては、以下の記事にまとめています。
[PHP] WEBアプリケーションの脆弱性 - XSS(クロスサイトスクリプティング)脆弱性と対策について

SQLインジェクションとは

SQLインジェクションの仕組みはとても単純で、悪意のある攻撃者が脆弱性のあるWEBサイトを見つけ、「サイト上の入力フォーム等から、サイト側の意図しないSQLを送信し、不正なクエリを実行する」というものです。

仕組みは単純ですが、その種類と想定される被害は多岐にわたり、WEBサイトに深刻な被害をもたらします。

SQLインジェクションの概要図

SQLインジェクション脆弱性のコード

もっとも簡単な例でSQLインジェクションの脆弱性を再現してみます。

コード例

test.html

以下はユーザー名とパスワードを入力してログインするための画面です。

<html>
  <head>
    <title>SQLインジェクション検証</title>
  </head>
  <body>
    <h1>ログイン画面</h1>
    <form action="/login.php" method="POST">
      ユーザー名:<input type="text" name="email"/><br>
      パスワード:<input type="text" name="password"/><br>
      <input type="submit" value="ログイン"/>
    </form>
  </body>
</html>
画面イメージ

login.html

ログイン画面から送信されたユーザー名とパスワードで認証を行う、以下のような処理があるとします。

<html>
  <head>
    <title>SQLインジェクション検証</title>
  </head>
  <body>
    <h1>認証</h1>
    <?php
      $pdo = new PDO("mysql:host=mysql;dbname=test", 'user', 'password');

      $email = $_POST["email"];
      $password = $_POST["password"];

      $sql = "
select *
from   users
where  email = '{$email}'
and    password = '{$password}'";

      $users = $pdo->query($sql);
      $user = $users->fetch();

      echo "<h2>認証結果</h2>";
      if ($user) {
        echo "<div style=\"color: #00f;\"><b>認証成功</b></div>";
        echo "Eメール:{$user['email']}<br>";
        echo "名前:{$user['name']}<br>";
      } else {
        echo "<div style=\"color: #f00;\"><b>認証失敗</b></div>";
      }

      echo "<h2>実行したSQL</h2>";
      echo "<pre style=\"border: 1px solid #aaa; padding: 10px;\">{$sql}</pre>";

    ?>
  </body>
</html>

ここでは、受け取ったusernameとpasswordを使ってusersテーブルを検索し、該当レコードが存在すれば認証成功としてユーザー情報を表示しています。

なお、確認用に実行したSQL文をそのまま表示しています。

データベース

usersテーブル

ユーザー情報は以下のようなusersテーブルを使用します。

実行例

制作者側が意図したログイン認証

SQLインジェクション脆弱性を突いた攻撃

パスワードに「' or '1' = '1」という文字列を入力して送信します。

usersテーブルの検索に使用しているSQLに、入力されたパラメータを展開すると、以下のようなクエリになります。

元のSQL
select *
from   users
where  email = '{$email}'
and    password = '{$password}'
変数展開後のSQL
select *
from   users
where  email = ''
and    password = '' or '1' = '1'

こうすることで攻撃者は、SQLのWHERE句を強制的にTrueにすることで、認証を突破することができてしまいます。この後はなりすましや個人情報の搾取といった被害が考えられます。

また、この例では1件のレコードを使って認証を行っていますが、「' or id = '4」などとすることで、任意の会員IDで認証を行うこともできてしまいます。

その他の想定されるケース

データ破壊・改ざん

攻撃者は脆弱性を利用してSQLを自由に発行することができます。以下のようにSQLを区切って、データの削除や改ざんなどされる恐れがあります。

入力パラメータ:'; truncate table users;

select *
from   users
where  email = ''
and    password = ''; truncate table users;'

SQLインジェクション対策

プリペアードステートメント

SQLインジェクション対策はいくつかありますが、まず必須で対応しなくてはならないのがプリペアードステートメントです。ログイン認証のSQLの組み立てから発行部分を以下のようにします。

login.html - SQLの組み立てから発行部分

      $sql = "
select *
from   users
where  email = :email
and    password = :password";

      $stmt = $pdo->prepare($sql);
      $stmt->bindParam(':email', $email);
      $stmt->bindParam(':password', $password);

      $stmt->execute();
      $user = $stmt->fetch();

変数に置き換えたい部分をプレースホルダ(:xxx)といい、bindParamでパラメータを設定してやることで、発行時に適切にエスケープして実行してくれるので、不正なクエリが発行されるのを防ぐことができます。

なお、フレームワークを使用している場合であっても、文字列内で変数を展開するようなクエリは脆弱性となるため、適切にバインドする必要があります。

動作確認

入力値チェック

ユーザーからの入力値は適切な値が入力されているかのチェック(バリデーション)を行う必要があります。Laravelなどフレームワークを使用している場合は、バリデーションクラスが用意されているので、適切に使用することが望ましいです。

WAF(Web Application Firewall)

クラウドを使用している場合は、WAFを導入するのも有効です。WAFを導入することで、一般的なWEBアプリケーションの脆弱性を利用した攻撃をブロックすることができるため、SQLインジェクション以外の攻撃にも非常に有効です。

コメント

このブログの人気の投稿

docker-compose up で proxyconnect tcp: dial tcp: lookup proxy.example.com: no such host

docker-compose で起動したweb、MySQLに接続できない事象

【PHP】PHP_CodeSnifferを使う(コーディングルールのカスタマイズ)