皆さん、お元気ですか?GMOクラウドの縞子です。
この記事は2部構成で、前編では「クロスサイト・スクリプティング」について、紹介いたしました。前回の記事はこちらです。
後編は「SQLインジェクション」をご紹介させていただきます。
- Ⅲ. SQLインジェクション
- Ⅲ-Ⅰ. 攻撃手順と影響
- Ⅲ-Ⅱ. 対策方法
- Ⅲ-Ⅲ. 脆弱性確認例と対策手順
- Ⅳ. 最後に
- Ⅴ. 参考情報
- 連載『Webサイト開発時のセキュリティ対策』のほかの記事はこちら
第1回:Webサイト開発時のセキュリティ対策(前編) - 第2回:この記事
Ⅲ.SQLインジェクション
続いてSQLインジェクションについて紹介します。
Ⅲ-Ⅰ.攻撃手順と影響
データベース(DB)と連携して動作するWebサイトでは、外部から入力された値を元にSQL文(データベースへの命令文)を組み立てることがよくあります。SQLインジェクション脆弱性を持つサイトでは、入力された値によっては意図しないSQLが生成され、結果として不正にDBのデータが読み取られたり、データが改ざんまたは削除されたりするなどの被害をこうむる可能性があります。
Ⅲ-Ⅱ.対策方法
入力された値をそのまま使用して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パラメーターに何も渡さなかった場合、次のように表示されます。
では、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>
※2015.6.15 編集部注
一部記事内容に関してご指摘をいただきましたので、修正させていただきました。
修正したWebページを、先ほど情報漏えいが起きたURL(http://xxx.yyy.zzz/cats.php?type=%' union select * from users; /*%')で再び開いてみます。
データの取得ができませんでした。不適切なtypeパラメーター値が渡された場合は、ページへの値出力は行うべきではないため期待通りの結果です。