エンジニアのspin13です。
今回はSQLインジェクションについてです。
SQLインジェクションとは
入力パラメータの検証不備等の原因で、予期せぬSQL文が発行されてしまう 予想していないパラメータが注入(inject)される
SQLインジェクションで何が起こるのか
- データの窃盗
- データの改変 SQL文で書けるようなことはなんでも起こり得る
例
<?php if(!empty($_POST)){ $db = new PDO('mysql:dbname=test_db;host=localhost', 'user', 'password'); $user = $_POST["user"]; $pass = $_POST["pass"]; echo "発行クエリ<br>"; $query = "SELECT * FROM users WHERE name='{$user}' AND pass='{$pass}'"; echo $query; echo "<br>"; $result = $db -> query($query) -> fetchAll(); echo "<br>"; if($result){ echo "成功"; }else{ echo "失敗"; } } ?>
上記はPOSTでユーザとパスワードを受け取ってDBに該当ユーザのレコードが存在するかどうかを確認するSQL文を発行します。
ユーザに user1
パスワードに password
と入力してsubmitしてみます。
出来上がるSQL文は、
SELECT * FROM users WHERE name='user1' AND pass='password'
です。
[test_db]> select * from users; +------+-------+-----------+ | id | name | pass | +------+-------+-----------+ | 1 | user1 | password | | 2 | user2 | changed | | 3 | user3 | password3 | +------+-------+-----------+
このDBに対してSQLを実行します。
[test_db]> SELECT * FROM users WHERE name='user1' AND pass='password'; +------+-------+----------+ | id | name | pass | +------+-------+----------+ | 1 | user1 | password | +------+-------+----------+ 1 row in set (0.01 sec)
データが存在するのでログインに成功します。
[test_db]> SELECT * FROM users WHERE name='hoge' AND pass='fuga'; Empty set (0.00 sec)
存在しないユーザとパスワードの組み合わせではもちろんレコードが存在しないので失敗します。
次にSQLインジェクションで強制的にログインしてみます。
ユーザに user1 ' AND 1=1 -- '
パスワードに hoge
と入力してsubmitしてみます。
出来上がるSQL文は
SELECT * FROM users WHERE name='user1 ' AND 1=1 -- '' AND pass='hoge'
さてDBで叩いてみましょう。
[test_db]> SELECT * FROM users WHERE name='user1 ' AND 1=1 -- '' AND pass='hoge'; -> ; +------+-------+----------+ | id | name | pass | +------+-------+----------+ | 1 | user1 | password | +------+-------+----------+
user1のレコードが返ってきてしまうんです!
なぜこのようなことが起きてしまうのでしょう。
解説
受け取ったパラメータをPHPのプログラム側で
$query = "SELECT * FROM users WHERE name='{$user}' AND pass='{$pass}'";
として展開しています。
userに user1 ' AND 1=1 -- '
と入れると、
name = ' user1 ' AND 1=1 -- '
SQLでは --
以降はコメントとして扱われるので、
AND pass='{$pass}'"
が無視されてしまうのでパスワードに何が入っていても関係なくなってしまうんですね。
対策
プレースホルダを使いましょう
$state = $db-> prepare("SELECT * FROM users WHERE name=? AND pass=?"); $state-> execute(array($user, $pass));
このようにすればSQLの本体部分とパラメータ部で切り離されて、プレースホルダで与える部分は値としてしか解釈されないのでSQLインジェクションで不正なパラメータを注入するのが困難になります。
不正な値を受け取って実行できる状態でなくするのが大切です。 最近ではライブラリ側が自動でエスケープ処理等をしてくれるので意識することは少なくなっていますが、原理を知っておくときっと何か役に立ちます。
おまけ
user: aaaaaa' ;UPDATE users SET pass='' -- ' pass: aaaa
SELECT * FROM users WHERE name='aaaaaa' ;UPDATE users SET pass='' -- '' AND pass='aaa'
[test_db]> select * from users; +------+-------+------+ | id | name | pass | +------+-------+------+ | 1 | user1 | | | 2 | user2 | | | 3 | user3 | | +------+-------+------+ 3 rows in set (0.00 sec)
▂▅▇█▓▒░(’ω’)░▒▓█▇▅▂