本コラムは、GMOクラウド株式会社の特設サイトに掲載されている「GMOクラウドアカデミー ~あなたの興味が知識に変わる」より転載したものです。GMOクラウドアカデミーは、クラウド・ホスティングの「悩み」「問題」「課題」の解決を目的に、現場の声を集約した「生きた」コンテンツを提供します。

皆さん、お元気ですか?GMOクラウドの縞子です。 この記事は2部構成で、前編では「クロスサイト・スクリプティング」について、紹介いたしました。前回の記事はこちらです。 後編は「SQLインジェクション」をご紹介させていただきます。

Ⅲ. SQLインジェクション
Ⅲ-Ⅰ. 攻撃手順と影響
Ⅲ-Ⅱ. 対策方法
Ⅲ-Ⅲ. 脆弱性確認例と対策手順
Ⅳ. 最後に
Ⅴ. 参考情報

Ⅲ.SQLインジェクション

続いてSQLインジェクションについて紹介します。

Ⅲ-Ⅰ.攻撃手順と影響

データベース(DB)と連携して動作するWebサイトでは、外部から入力された値を元にSQL文(データベースへの命令文)を組み立てることがよくあります。SQLインジェクション脆弱性を持つサイトでは、入力された値によっては意図しないSQLが生成され、結果として不正にDBのデータが読み取られたり、データが改ざんまたは削除されたりするなどの被害をこうむる可能性があります。

図1.SQLインジェクションの攻撃手順

Ⅲ-Ⅱ.対策方法

入力された値をそのまま使用してSQL文を組み立てることが問題の原因ですので、DBのプリペアドステートメント(準備された文)という仕組みを使用してSQL文を組み立てるようにします。 プリペアドステートメントとは、実行したいSQLをコンパイルした一種のテンプレートのようなものであり、テンプレート中に変数の場所を示す箇所(プレースホルダ)を置いておき、プログラム実行時に実際の値をセット(バインド)します。 プリペアドステートメントを用いることで想定外のSQL文が組み立てられる現象を回避できるため、SQLインジェクションの根本的な対策となります。

Ⅲ-Ⅲ.脆弱性確認例と対策手順

SQLインジェクション脆弱性を持ったテストページを用意し、インジェクションが行われた場合の挙動を確認してみます。テストページでは猫の一覧情報をDBから取得して表示します。 ページはPHPで実装し、DBとの接続にはPDOを使用します。

●cats.php

<?php header("Content-type: text/html; charset=UTF-8"); ?>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body>
 <h2>猫一覧表</h2>
 <table border="1">
 <tr>
  <th>ID</th>
  <th>名前</th>
  <th>種類</th>
 </tr>
 <?php
 mb_regex_encoding("UTF-8");

 // GET メソッドで type パラメーター値を取得
 // % や _ など MySQL のワイルド カード文字はエスケープ処理をします
 $type = mb_ereg_replace('([_%#])', '#¥1', $_GET['type']);

 $dsn = "mysql:dbname=Test;host=localhost;charset=utf8;";
 $user = "testuser";
 $pass = "pass";

 try{
  $dbh = new PDO($dsn, $user, $pass);
  $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  $sql = "select * from cats where type like '%" . $type . "%'";

  foreach ( $dbh->query($sql) as $row ) {
   echo "<tr>";
   echo "<td>" . htmlspecialchars($row['id']) . "</td>";
   echo "<td>" . htmlspecialchars($row['name']) . "</td>";
   echo "<td>" . htmlspecialchars($row['type']) . "</td>";
   echo "</tr>";
  }
  }catch (PDOException $e){
  echo 'Error: ' . htmlspecialchars($e->getMessage());
  die();
  }

  $dbh = null;
 ?>
 </table>
</body>
</html>

上のcats.phpページを開く際、typeパラメーターに何も渡さなかった場合、次のように表示されます。

では、typeパラメーターに「%' union select * from users; /*%'」というSQL文の一部をセットしたURLを指定してみます。 URL : http://xxx.yyy.zzz/cats.php?type=%' union select * from users; /*%'

前述のURLをブラウザから開くとWebページ内にユーザー名とパスワードが見えてしまい、情報漏えいが起きてしまいました。 これはtypeパラメーターへの入力値により、cats.phpの中では下のSQL文が組み立てられたことに起因しています。

select * from cats where type like '%%' union select * from users; /*%'

上のSQL文はunion によってcatテーブルとusersテーブルを結合して表示させるものであり、これが実行されたためにユーザー情報が取得できてしまいました。 なお、もし同じような手順でDBスキーマやテーブルの削除を行うSQL文(drop databaseやdrop table)が渡された場合、DBの権限設定によりますが、スキーマもしくはテーブルが削除される可能性があります。怖いですね。

次にGETで渡された値をそのままSQL文に含めず、プレースホルダを使用してSQL文を組み立てる手順を確認してみます。 PDOでは、"?" または ":{キーワード}" を使ってSQL文の中にプレースホルダを指定することができます。

PDO::prepare()を呼び出してプレースホルダを含むSQL文を実行する準備をし、 PDOStatement::execute()でプレースホルダに値をセットした上でDBへの問い合わせを実行します。問い合わせた結果はPDOStatement::fetch()で一行ずつ取り出します。

●cats.php(修正版)

<?php header("Content-type: text/html; charset=UTF-8"); ?>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body>
 <h2>猫一覧表</h2>
 <table border="1">
<tr>
  <th>ID</th>
  <th>名前</th>
  <th>種類</th>
 </tr>
 <?php
 mb_regex_encoding("UTF-8");

 // GET メソッドで type パラメーター値を取得
 // % や _ など MySQL のワイルド カード文字はエスケープ処理をします
 $type = mb_ereg_replace('([_%#])', '#¥1', $_GET['type']);

 $dsn = "mysql:dbname=Test;host=localhost;charset=utf8;";
 $user = "testuser";
 $pass = "pass";

 try{
  $dbh = new PDO($dsn, $user, $pass);
  $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

  $sql = "select * from cats where type like :type escape '#'";
  $sth = $dbh->prepare($sql);
  $sth->execute(array(":type" => "%$type%"));

  while ( $row = $sth->fetch(PDO::FETCH_ASSOC) ) {
   echo "<tr>";
   echo "<td>" . htmlspecialchars($row['id']) . "</td>";
   echo "<td>" . htmlspecialchars($row['name']) . "</td>";
   echo "<td>" . htmlspecialchars($row['type']) . "</td>";
   echo "</tr>";
  }
 }catch (PDOException $e){
  echo 'Error: ' . htmlspecialchars($e->getMessage());
  die();
 }

 $dbh = null;
 ?>
 </table>
</body>
</html>

データの取得ができませんでした。不適切なtypeパラメーター値が渡された場合は、ページへの値出力は行うべきではないため期待通りの結果です。

Ⅳ.最後に

テストページを作るにあたり、猫一覧テーブルにしたことは完全に個人的趣味です。体が弱っていると自制心も弱まるという良い例でした。犬派の方にはごめんなさい。 それはさておき、今回紹介したクロスサイト・スクリプティングとSQLインジェクションは、どちらも入力値をそのまま処理に使用することが問題となっています。外部から取得した値が想定したものかどうか、想定外のものだった場合はどうするのか、開発時には常に考えることが大事なのかも知れませんね。

Ⅴ.参考情報

※ご利用中のクラウドサービスによっては、利用できない場合があります。

この記事を書いた人 GMOクラウド 縞子 GMOクラウド株式会社 企画開発部 開発グループ所属。2014 年に同社へ入社し、主に SaaS開発に携わる。これまで使用して来た言語は C/C++、PHP、Java、JavaScript。猫派ですが、肉球のある生物はみんな好きです。 GMOクラウド 縞子の記事一覧
本コラムは、GMOクラウド株式会社の特設サイトに掲載されている「GMOクラウドアカデミー ~あなたの興味が知識に変わる」より転載したものです。GMOクラウドアカデミーは、クラウド・ホスティングの「悩み」「問題」「課題」の解決を目的に、現場の声を集約した「生きた」コンテンツを提供します。