この記事は、North Detail Advent Calendar 2019 の21日目の記事です。
はじめに
先日、社内で「安全なウェブサイトの作り方」を使った読書会が行われました
SQLインジェクションについて議題に上がったので、
勉強がてら実際に試してみよう!というのが今回の趣旨になります。
SQLインジェクション?
SQLインジェクション(英: SQL Injection)とは、
アプリケーションのセキュリティ上の不備を意図的に利用し、
アプリケーションが想定しないSQL文を実行させることにより、
データベースシステムを不正に操作する攻撃方法のこと。
「Injection」という単語が「注入する」という意味のようです(知らなかった)。
アプリケーションが実行するSQL文に変なものを注入して
意図しない動作をさせる攻撃なので「SQL Injection」ってことですね
実際に起こしてみる
データを作る
まずはMySQLにテスト用データを作成します。
CREATE TABLE users (
id int NOT NULL AUTO_INCREMENT,
name varchar(10),
PRIMARY KEY (id)
);
INSERT INTO
users(name)
VALUES
('フグ田サザエ'),
('フグ田マスオ'),
('磯野波平'),
('磯野フネ'),
('磯野カツオ'),
('磯野ワカメ'),
('フグ田タラオ'),
('タマ')
;
テスト用画面を作る
次は、IDを入力してユーザーを検索できる画面を作ります。
PDOクラスを使ってMySQLにアクセスしています。
<?php
try {
$records = [];
$id = $_POST['id'];
$pdo = new PDO(
'DSN',
'USERNAME',
'PASSWORD'
);
$prepare = $pdo->prepare('SELECT * FROM users WHERE id = '. $id. ';');
$prepare->execute();
$records = $prepare->fetchAll(PDO::FETCH_ASSOC);
} catch(PDOException $e) {
echo $e->getMessage();
}
?>
うーんシンプル。
ここで…文字列結合を使ってクエリ組み立てていますね
$prepare = $pdo->prepare('SELECT * FROM users WHERE id = '. $id. ';');
$prepare->execute();
攻撃してみる
先ほどの例では「3」と検索して「磯野波平」のデータを取得しました。
じゃあ次は「0 OR TRUE; --」で…
users
テーブルの全レコードがヒットしてしまいました
ユーザー入力をそのまま採用した結果、こんなSQL文になりました。
WHERE句が常にTRUEになるので、全てのレコードが対象になったんですね。
SELECT * FROM users WHERE id = 0 OR TRUE; --;
ちょっと付け加えて、お次は「0 OR TRUE; DELETE FROM users; --」で検索…
あれ?さっきと同じ??
MySQLを覗いてみると…
な ん と い う こ と で し ょ う
匠の手によって全てのレコードが削除されてしまいました
先ほどの例とだいたい同じですが、
検索クエリを正常終了させた後にDELETE文が実行されちゃってますね…
SELECT * FROM users WHERE id = 0 OR TRUE; DELETE FROM users; --;
対策する
SQL 文の組み立ては全てプレースホルダで実装する。
「安全なウェブサイトの作り方」ではmysqliが使われていましたが、PDOでもプレースホルダを使って実装できます!
先ほどは文字列結合を使ってクエリを組み立てていましたが、
今度は「:id」のようにコロン+パラメータ名
の形でクエリ内に記載します。
そしてクエリ実行の前に bindValue
関数を使ってバインドします。
(bindParam
関数でもバインドできます。 変数が評価されるタイミングが異なるそうです。)
$prepare = $pdo->prepare('SELECT * FROM users WHERE id = '. $id. ';');
$prepare->execute();
$prepare = $pdo->prepare('SELECT * FROM users WHERE id = :id;');
$prepare->bindValue(':id', $id, PDO::PARAM_INT);
$prepare->execute();
プレースホルダを使ってバインドすると、クエリとして解釈されないようです。
あくまで「値」として扱われるため、変なものを注入した場合でもクエリとして実行されないんですね。
実際にさっきの攻撃を試してみましょう
何も検索されなくなりました!
今度は磯野家も無事でした…
まとめ
SQLインジェクションによる攻撃と、その対策方法について実際に試してみました。
SQLインジェクションやプレースホルダによる対策自体は知っていて、学生時代にJavaで一度やったことある程度でした。
その時は理解が浅い状態だったので、仕組みも含めて復習できて良かったです
明日は
「North Detail Advent Calendar」 22日目は @yamatohkd さんです!
→ スマホカメラで手のモーションを記録してUnityでピアノ演奏したかった
参考
-
安全なウェブサイトの作り方
https://www.ipa.go.jp/security/vuln/websecurity.html -
PDOStatementクラス
https://www.php.net/manual/ja/class.pdostatement.php -
サザエさんの登場人物
https://ja.wikipedia.org/wiki/サザエさんの登場人物