先日,MySQL 5.0 および 4.1 に関してSQLインジェクションの危険性に対応するためにバージョンアップが行われた。

 SQLインジェクションとは,入力されたデータにより意図せざるSQL文が実行されてしまうという攻撃である。データベースのデータを書き換えられたり,データが読み出されることにより情報が漏洩したりする恐れがある。セキュリティ・ホールというとWebサーバーやWebアプリケーション言語の専売特許だという印象があるが,データベースにも存在する。十分な注意が必要である。

 以下,SQLインジェクションの原理と,MySQLに存在した問題の詳細とその対応について解説する。問題の発見から修正にいたるやりとりはWeb上で公開されており,誰がいつ問題を指摘し修正したのかもたどることができる。

mysqli_real_escape_string()関数によるSQLインジェクションの防止

 プログラムにおいて,SQL文によるデータベース処理とは,入力した値(文字列や数値)を用いてSQL文を作成し,データベースで実行することだ。SQLインジェクションは,入力した文字列にデータベースサーバーが誤った動作を引き起こすような文字列を含むことで発生する。

 例として,「名前」の入力を受けてSQL文を組み立てる場合を解説する。

リスト1●SQLインジェクションのサンプルコード(PHP 5.0)

1. 2. $mysqli = mysqli_init();
3. $mysqli->options(MYSQLI_READ_DEFAULT_FILE,"C:\my_sqltest.ini");
4. $mysqli->real_connect('localhost','root','password','databank');
5.
6. $sql = "SELECT * FROM member"; ---テーブルのレコードを全て表示
7. $res = $mysqli->query( $sql ) ;
8. echo $sql .'
' ;
9. while ( $row = $res->fetch_array(MYSQL_ASSOC)) {
10. echo $row["no"] . " -- " . $row["name"] . " -- " . $row["tel"]."
" ;
11. }
12. echo '
' ;
13.
14. $input1 = '佐藤' ; ---(1)通常の入力例
15. $sql = "SELECT * FROM member WHERE name = '" . $input1 . "'";
16. $res = $mysqli->query( $sql ) ;
17. echo $sql .'
' ;
18. while ( $row = $res->fetch_array(MYSQL_ASSOC)) {
19. echo $row["no"] . " -- " . $row["name"] . " -- " . $row["tel"]."
" ;
20. }
21. echo '
' ;
22.
23. $input2 = "佐藤' or name != '鈴木" ; ---(2)悪意のある入力例
24. $sql = "SELECT * FROM member WHERE name = '" . $input2 ."'";
25. $res = $mysqli->query( $sql ) ;
26. echo $sql .'
' ;
27. while ( $row = $res->fetch_array(MYSQL_ASSOC)) {
28. echo $row["no"] . " -- " . $row["name"] . " -- " . $row["tel"]."
" ;
29. }
30. echo '
' ;
31.
32. $sql = "SELECT * FROM member WHERE name = '". $mysqli->real_escape_string( $input2 ) ."'"; ---(3)誤動作を起こしそうな文字を無効化
33. $res = $mysqli->query( $sql ) ;
34. echo $sql .'
' ;
35. while ( $row = $res->fetch_array(MYSQL_ASSOC)) {
36. echo $row["no"] . " -- " . $row["name"] . " -- " . $row["tel"]."
" ;
37. }
38. echo '
' ;
39.
40. $mysqli->close() ;
41. ?>

 このサンプルコードでは,6行目,14~15行目,23~24行目,32行目の各行でSQL文を作成している。

 14~15行目では,変数$input1にセットした文字列を使用して,nameフィールドがマッチするレコードを検索するSQL文を作成している。変数$input1には,「佐藤」をセットしており,nameフィールドが「佐藤」であるレコードの検索を実施する。

 23~24行目は,悪意のある入力例だ。変数$input2には,「佐藤' or name != '鈴木」をセットしており,nameフィールドが「佐藤」であるか「鈴木」でないレコードの検索を実施する。つまり,大半のレコードが該当することになる。これが実行されると,他のユーザーの情報が表示される,情報漏洩につながりかねない。

 32行目は,mysqli_real_escape_string()関数を使用して変数$input2を変換,誤動作を起こしそうな文字を無効化している。

 リスト2のサンプルコードの実行結果を見ると,その違いが一目瞭然だ。

リスト2●サンプルコードの実行結果

SQL Test
SELECT * FROM member ---テーブルのレコードを全て表示
1 -- satou -- 03-34
2 -- suzuki -- 117
3 -- tanaka -- 110
9 -- 豊田 -- 800
5 -- takahas -- 104
6 -- 佐藤 -- 177
7 -- 鈴木 -- 123
8 -- 本田 -- 750
999 -- MySQL -- 03-12

SELECT * FROM member WHERE name = '佐藤' ---(1)通常の入力例
6 -- 佐藤 -- 177

SELECT * FROM member WHERE name = '佐藤' or name != '鈴木' ---(2)悪意のある入力例
1 -- satou -- 03-34
2 -- suzuki -- 117
3 -- tanaka -- 110
9 -- 豊田 -- 800
5 -- takahas -- 104
6 -- 佐藤 -- 177
8 -- 本田 -- 750
999 -- MySQL -- 03-12

SELECT * FROM member WHERE name = '佐藤\' or name != \'鈴木' ---(3)誤動作を起こしそうな文字を無効化

 まず,(1)(ソースでは14~15行目)は,プログラマが想定した通常パターンだ。名前を入力することによって,それに対応するレコードをデータベースから検索し表示している。

 (2)(ソースでは23~24行目)のSQL文は,悪意のある入力を受けて,テーブルに格納しているレコートの大半が表示されている。このようにSQLサーバーに誤動作を起こさせる攻撃がSQLインジェクションだ。防止するには,誤動作を起こしそうな文字を無効にする必要がある。

 (3)(26~27行目)のSQL文では,mysqli_real_escape_string()関数を使用している。mysqli_real_escape_string()関数は,誤動作を起こす文字「’」を「\’」に変更している。そのため,データベースサーバーは,誤動作を起こすことなく,検索を終了している。

 これは,非常に単純な例ではあるが,DROPやDELETEステートメントが混入していた場合,非常に深刻である。しかしながら,MySQLは,標準設定では,複数のSQL文を同時に処理するようになっていない。そのため,DROPやDELETEなどのステートメントを混入しても動作しない。