2015.06.08

SQLインジェクションとは?Webサイトのセキュリティ対策を学ぼう

難易度
3
カテゴリー
やってみよう!
タグ
Web開発
セキュリティ

150608_security_mv

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

Ⅲ.SQLインジェクション

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

Ⅲ-Ⅰ.攻撃手順と影響

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

150608_security_mv図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>

※2015.6.15 編集部注
一部記事内容に関してご指摘をいただきましたので、修正させていただきました。

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

150608_security_img1

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

150608_security_img2

前述の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>

※2015.6.15 編集部注
一部記事内容に関してご指摘をいただきましたので、修正させていただきました。

修正したWebページを、先ほど情報漏えいが起きたURL(http://xxx.yyy.zzz/cats.php?type=%' union select * from users; /*%')で再び開いてみます。

150608_security_img3

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

Ⅳ.最後に

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

この記事を書いた人

縞子 GMOグローバルサイン・HD

GMOクラウドアカデミーYouTubeチャンネルはこちらから

アカデミー用バナー

メルマガ会員募集中!

アカデミーの最新情報や会員限定のお得な情報をお届けします。

メルマガ登録はこちら