MySQLとPHPにおけるSQLインジェクション対策について
サイオンコミュニケーションズ株式会社
上里 彦司
1.はじめに
こんにちは。今回はSQLインジェクションの対策について少しまとめてみました。
SQLインジェクションはWebアプリケーションに対する攻撃手法の一つで、セキュリティ上の不備を意図的に利用して、SQLクエリを改ざんしたり、想定しないSQL文を実行させることで、データベースを不正に操作する攻撃と、それを可能にする脆弱性の事です。
検証に使用したコードの動作確認環境
サーバ
- Windows XP Professional
- Apache 2.0.59
- PHP 5.2.3
- MySQL 5.0.41
クライアント
- Windows XP Professional
- IE6
2.SQLインジェクションの例
まず、SQLインジェクションの例について少しご説明します。
テーブル構成:testTable
| フィールド | 種別 |
|---|---|
| id | int(4) |
| name | varchar(255) |
// GET変数をSQL文のWHERE節に挿入する $sql = "SELECT * FROM testTable WHERE id = '" . $_GET["id"] . "'"; // 以下のようなクエリを実行させることを想定。 // "SELECT * FROM testTable WHERE id = '1'" // ここでSQLインジェクションが発生する危険があります $inject = mysql_query($sql);
上の例の場合、$_GET[”id”] に ‘0′や’1′などの値が入ることを想定したプログラム
ですが、もし、$_GET[”id”] に “0′ OR id != ‘0″という値を入れられると、testTable
の全データが表示される危険があります。
3.SQLインジェクション対策
その1:入力値のチェック
まず変数に期待した値が入ってるか、入力値のチェックをしてみましょう。
この時のチェックには
| is_numeric() | 変数が数字または数値文字列であるかを調べます。 |
| is_int(), is_float() | 与えられた変数が数値型、float型かどうかを調べます。 |
| ctype_digit() | 与えられた文字列のすべての文字が 数字であるかどうかを調べます。 |
| strlen() | 文字列の長さを数えます。 |
| mb_strlen() | マルチバイト文字の一文字は1個として数えます。 |
や正規表現を使った関数等を使用すると便利です。では少し使ってみましょう。
<?php
$num = 28;
$str = "28";
$float = 28.0;
$mb_str = "SQLインジェクション";
print "check1:";
if (is_numeric($num)) {
print "true <br />n";
} else {
print "false <br />n";
}
print "check2:";
if (is_numeric($str)) {
print "true <br />n";
} else {
print "false <br />n";
}
print "check3:";
if (is_int($num)) {
print "true <br />n";
} else {
print "false <br />n";
}
print "check4:";
if (is_int($str)) {
print "true <br />n";
} else {
print "false <br />n";
}
print "check5:";
if (is_float($num)) {
print "true <br />n";
} else {
print "false <br />n";
}
print "check6:";
if (is_float($float)) {
print "true <br />n";
} else {
print "false <br />n";
}
print "check7:" . strlen($str) . "<br />n";
print "check8:" . strlen($mb_str) ."<br />n";
print "check9:" . mb_strlen($str) . "<br />n";
print "check10:" . mb_strlen($mb_str, "UTF-8") . "<br />n";
?>
出力結果は、
check1:true check2:true check3:true check4:false check5:false check6:true check7:2 check8:27 check9:2 check10:11
になります。こういった関数を使って期待していない値やパターンをチェックすることができます。
その2:エスケープ処理
SQL文におけるエスケープ処理とは?
・クエリ中で使用される特殊文字の効果をキャンセルして通常の文字として扱えるようにします。
(エスケープ文字には「\」が使われることが多いです)
例:
in "Okinawa" => エスケープ処理 => in "Okinawa" It's salt => エスケープ処理 => It's salt
次にデータ型に関わらずクエリとなる変数の全てにエスケープ処理を施してみましょう。
全てのユーザ入力をそれぞれのデータベースの為に用意されているエスケープ関数でエスケープします。
(今回はMySQLを使用していますのでmysql_real_escape_string()がそれに当たります)
では実際に使用した場合の例を、先ほどのSQLインジェクションに適用した例で見てみましょう。
まずはエスケープ処理を行わなかった場合です。
$_GET[”id”] に “0′ OR id != ‘0″ という値が挿入された場合、SQL文は
SELECT * FROM testTable WHERE id = ‘0′ OR id != ‘0′;
となり、全データを取得されてしまいます。
ここでmysql_real_escape_string()を使ってみましょう。「’」が「\」でエスケープされます。
$_GET[”id”] に “0′ OR id != ‘0″ を mysql_real_escape_string()でエスケープすると、
“0\’ OR id != \’0″ となります。この場合のSQL文は
SELECT * FROM testTable WHERE id = ‘0\’ OR id != \’0′;
となり、id列に “0″ というデータが入っている行を表示する(※)、という条件文になりますので、SQLインジェクションの危険は回避されます。
※本文はMySQLの事象です。MySQLでは、idのint型に自動的にキャストされる為、上の例では最初の「\」以下の文字列は無視されます。
ここで使った関数mysql_real_escape_string()がエスケープしてくれる特殊文字は
「\x00, \n, \r, \, ‘, “, \x1a」です。
mysql_real_escape_string()はこれらの文字の先頭に「\」を付加します。
その3:エスケープ処理の注意点
addslashes()やstr_replace()等でエスケープする事もあるかと思いますが、この場合不完全な場合が多々ある様なので(特殊文字やエスケープの方法がデータベースにより異なることがある為)、なるべくデータベース毎のエスケープ関数を利用した方が良いと思います。データベース毎に異なる特殊文字をそれぞれエスケープしてくれます。バイナリデータやマルチバイト文字をエスケープする場合にもデータベース毎のエスケープ関数が利用されます。
もうひとつ、2重にエスケープ処理をしないようにget_magic_quotes_gpc()等でmagic_quotes_gpcといった自動エスケ―プがonになっていないか確かめてみましょう。もしonならstripslashes()でエスケープ部分を取り除く事が必要です。
マジッククォートが有効になっている場合、付け加えられたバックスラッシュをすべて取り除く必要があります。そうしないとユーザの入力はすでにエスケープされているので、二重にエスケープされてしまうことになり、エスケープ記号の意味がなくなってしまうのです。
以下のようにチェックしてから、エスケープします。
// チェック
if (get_magic_quotes_gpc()) {
$name = stripslashes($_POST["name"]);//onなら「\」を取り除く
} else {
$name = $_POST["name"];
}
// データベース固有のエスケープ関数でエスケープ処理
$escaped_name = mysql_real_escape_string($name);
注:また文字コードにShift_jisを使用した場合に第3回「PHP とShift-JIS 環境での文字化けについて」で取り上げているように2バイト目が「5C」となる文字列をエスケープする場合に、「\」が「5C」で表わされる為に, mysql_real_escape_string()などでエスケープした場合にも「能」や「表」といった漢字は「能\」と「表\」になります(5C問題)。対策としては、他の文字コードを使う、一時的に変換してエスケープする、その為のエスケープ処理の関数を書く等が考えられます。
4.おわりに
今回は基本的な方法のご紹介でしたが、これらを施すだけでもSQLインジェクションに関する脆弱性はかなり軽減することと思われます。
またこの他に、prepared statement(mysqli拡張やpgsql拡張)を使用することでプリペアードクエリとして実行する方法があります。この様なインターフェースを用いると処理系がエスケープ処理を行ってくれる為,プリペアードクエリに渡されるパラメータは全て値として処理され、プリペアードクエリだけで実行した場合にはSQLインジェクションはほとんど不可能ということになります。もしサポートしているデータベースを利用されている場合は、エスケープ漏れの危険性がない分安全性は上がるので使用してみるのもいいかもしれません。以下はmysqli拡張での例です。
prepared statementとは?
・毎回 SQL 文をプログラムで生成する代わりに、前もって SQL 文の雛型を用意しておいて、
パラメータの部分だけを後で与えるというインタフェースです。
プリペアードクエリ(準備済み問い合わせ)を作成します。
「プレースホルダー」、「プリコンパイル」等の呼ばれ方もします。
<?php
// 以下のDBを用意します
$dbserver = "localhost";
$dbuser = "root";
$passwd = "admin";
$dbname = "test";
$id = 1000;
$mysqli = new mysqli($dbserver, $dbuser, $passwd, $dbname);
// プリペアードクエリを作成
$stmt = $mysqli->prepare("SELECT id, name FROM testTable WHERE id = ?");
// パラメータをバインド
$stmt->bind_param("i", $id);
// クエリ実行
$stmt->execute();
// 結果をそれぞれ変数にバインド
$stmt->bind_result($id, $userName);
// 値を取得します
$stmt->fetch();
printf("ID %d : NAME %s ", $userID, $userName);
// プリペアードクエリ・接続を閉じる
$stmt->close();
$mysqli->close();
?>
SQLインジェクションの多くはしっかりエスケープしていれば防げる脆弱性です。エスケープ漏れがないようにソースをチェックしてSQLインジェクション対策を施しましょう。
おまけ
その1:メール送信処理と文字化け対策
PHPからメールを送る際に文字化けを経験された事はありませんか?その場合、エンコーディングが正しく行われていれば文字化けを回避できます。
<?php
$to = "hoge@syon.co.jp";
$subject = "パーティーのお誘い";
$message = "じつは…今夜うちでパーティーがあるんだ!!";
// 使用言語と内部エンコーディングの設定を明示的に行う
mb_language("ja");
//スクリプトの文字コードと実行環境の文字コードが違う時に設定
mb_internal_encoding("UTF-8");
// メール送信
// 内部エンコーディング→ISO-2022-JP(JIS)/base64といった
// エンコーディングを自動的にしてくれる
if (mb_send_mail($to, $subject, $message) === false) {
print "送信に失敗。<br />n";
}
?>
その2:マルチバイト変換いろいろ
mb_convert_kana()の変換オプションを用いると様々な変換を行う事ができます。
<?php
// 文字コードはUTF-8
mb_regex_encoding("UTF-8");
mb_internal_encoding("UTF-8");
// 全角カタカナを全角ひらがなに変換
// (「ヴ、ヵ、ヶ」も含まれるが当然変換はされない。)
// 「ヴ」以外カタカナでないと思うのですが…(ry
if (mb_ereg("[ァ-ヶ]+", $before) !== false) {
$after = mb_convert_kana($before, "c", "UTF-8");
}
// 半角カタカナを全角ひらがなに変換(半角長音記号「ー」を含む)
if (mb_ereg("[ヲ-゚]+", $before) !== false) {
$after = mb_convert_kana($before, "H", "UTF-8");
}
// 全角英数字を半角英数字に変換
if (mb_ereg("[a-zA-Z0-9]+", $before) !== false) {
$after = mb_convert_kana($before, "a", "UTF-8");
}
?>























