【sqlインジェクション対策】徳丸先生に怒られない、動的sqlの安全な組み立て方...
TRANSCRIPT
copyright 2015 kuwata-lab.com all rights reserved.©
本発表の作成には徳丸先生は関わっておりません。 内容の責任はすべて発表者にあるので、 ご意見がありましたら先生にではなく 発表者までお願いします。
!! CAUTION !!
copyright 2015 kuwata-lab.com all rights reserved.©
発表の背景:裁判と損害賠償引用: http://blog.tokumaru.org/2015/01/sql.html
SQLインジェクション脆弱性が原因でクレジットカード情報が漏洩した事件につき、ショップ側が開発会社を相手取り損害賠償請求の裁判を起こし、ショップ側が勝訴
“
”結果、3131万9568円の損害を認定し、その3割を控除して、2262万3697円の損害賠償をY社に命じた
“”
copyright 2015 kuwata-lab.com all rights reserved.©
発表の背景:ばく大な金額引用: http://www.ipa.go.jp/files/000013778.pdf
(SQLiによる推計される被害額について) 推計すると、約 4,800 万円~1 億円程度を要するものと考えられる。
“”
copyright 2015 kuwata-lab.com all rights reserved.©
発表の目的✓SQL Injection が起きる真の原因を考える
「プレースホルダを使ってないから」では考察不足
✓SQL Injection を起こさない方法を考える 「プレースホルダを使え」では対策不足
copyright 2015 kuwata-lab.com all rights reserved.©
アジェンダ✓考察:なぜSQL Injectionが発生するのか?
✓対策:SQLテンプレート
✓対策:SQL構文木
✓まとめ
copyright 2015 kuwata-lab.com all rights reserved.©
前提知識✓SQL Injection が何か、知ってること
(以降では SQL Injection を SQLi と省略)
✓プレースホルダやバインド変数について 知ってること 「:id」や「?」がプレースホルダ、渡す値がバインド変数
✓PHPのコードが読めること PHPカンファレンスなので :)
copyright 2015 kuwata-lab.com all rights reserved.©
SQL Injection 対策エスケープしろ!プレースホルダを使え!
http://www.ipa.go.jp/files/000017320.pdf
copyright 2015 kuwata-lab.com all rights reserved.©
プレースホルダを使っててもSQLi
$sql = "select * from users" . " where deleted_at is null"; $vars = []; if ($max_age) { $sql .= " and age <= :max_age"; $vars['max_age'] = $max_age; } if ($min_age) { $sql .= " and age >= ".$min_age; $vars['min_age'] = $min_age; }
プレースホルダを 使っている
ただの文字列 連結! SQLi!
copyright 2015 kuwata-lab.com all rights reserved.©
なぜSQL Injectionが発生するのか?✓プレースホルダを使ってないから?
実情は、プレースホルダつきの安全なSQLを組み立てている最中に SQL Injection が入り込んでいる
✓
これこそが真の原因
文字列連結でSQLを組み立ててるから
copyright 2015 kuwata-lab.com all rights reserved.©
変数の埋め込みも文字列連結と同じ
// これと $sql .= " and age >= ".$min_age;
// これは同じ $sql .= " and age >= $min_age";
変数の埋め込みも 文字列連結と同じ
copyright 2015 kuwata-lab.com all rights reserved.©
なぜ文字列結合でSQLを作るのか?✓プレースホルダではできないことがあるから
要素数が動的に変わる in (....) 、order by でのカラム名指定
✓ 真の原因その2
// SQLを文字列で渡せてしまえるんだから、 // SQLを文字列結合で組み立ててしまうのは当然のこと $st = pdo->prepare("select * from users");
SQLを文字列で指定できてしまうから
copyright 2015 kuwata-lab.com all rights reserved.©
SQL Injection を防ぐには?✓文字列連結ではない方法で動的SQLを組み立てればよい 方法:SQLテンプレート or SQL構文木 (詳細は後述)
✓SQL文字列ではない方法でSQLを 指定できるようにすればよい 方法:SQL ID or SQL構文木 (詳細は後述)
copyright 2015 kuwata-lab.com all rights reserved.©
SQL Injection が発生する本当の原因は、 ライブラリや API のインターフェースに 欠陥があるからである。 間違いを誘発するものを使い続ける限り、 SQL Injection がなくなることはない。
Let's Tw
eet!
copyright 2015 kuwata-lab.com all rights reserved.©
ここまでのまとめ✓SQLを文字列連結で組み立てるから、 SQL Injection が発生する 対策:文字列連結ではない方法でSQLを組み立てる
✓生のSQLを文字列で渡せてしまうから、 文字列連結を使ってしまう 対策:生のSQLを渡すかわりに別の方法でSQLを指定する
✓本質的にはAPIやインターフェースの欠陥 結果はセキュリティ被害だけど、原因はセキュリティとは違う
copyright 2015 kuwata-lab.com all rights reserved.©
SQLテンプレートとは?✓SQLを生成するためのテンプレート
WebアプリでのHTMLテンプレートと同じ
✓SQL Injection が発生しない(原理上は) 文字列連結が不可、値のエスケープ or プレースホルダを強制
注:内部の実装では文字列連結を使うが、開発者が明示的に使うことはできない。
copyright 2015 kuwata-lab.com all rights reserved.©
サンプル:SQLテンプレート
/* 年齢の上限/下限が指定されたら、検索条件を追加 */
select * from users where deleted_at is null -- #if :max_age and age <= :max_age -- #end -- #if :min_age and age >= :min_age -- #end
copyright 2015 kuwata-lab.com all rights reserved.©
サンプル:PHPコードへ変換
/* 年齢の上限/下限が指定されたら、検索条件を追加 */
select * from users where deleted_at is null <?php if ($vars['max_age']) { ?> and age <= :max_age <?php } ?> <?php if ($vars['min_age']) { ?> and age >= :min_age <?php } ?>
注:実際はもう少し複雑なコードに変換される
copyright 2015 kuwata-lab.com all rights reserved.©
サンプル:生成されたSQL
// $var = array("min_age"=>20) なら
select * from users where deleted_at is null and age >= :min_age
copyright 2015 kuwata-lab.com all rights reserved.©
SQLTempl8✓SQLを対象としたテンプレートエンジン
https://github.com/kwatch/sqltempl8/
✓まだコンセプト実装 (いろいろ足りない) 使い方は随時変わるので、最新のドキュメントを参照のこと
copyright 2015 kuwata-lab.com all rights reserved.©
SQLTempl8:SQLを生成
1: <?php 2: require_once 'sqltempl8.php'; 3: $t = new SQLTempl8("sql/users.sql"); 4: $vars = array('min_age'=>20); 5: $sql = $t->render($vars); 6: echo $sql; //=> select * from users // where deleted_at is null // and age >= :min_age
copyright 2015 kuwata-lab.com all rights reserved.©
SQLTempl8:SQLを実行
1: <?php 2: require_once 'sqltempl8.php'; 3: $t = new SQLTempl8("sql/users.sql"); 4: $vars = array('min_age'=>20); 5: $pdo_conn = new PDO("....."); 6: $pdo_stmt = $t->execute($pdo_conn, $vars); 7: if ($pdo_stmt !== null) { 8: foreach ($pdo_stmt as $row) { ... } 9: }
copyright 2015 kuwata-lab.com all rights reserved.©
なぜSQL Injectionが発生しないの?
// SQLテンプレート
select * from users where deleted_at is null -- #if :max_age and age <= :max_age -- #end -- #if :min_age and age >= :min_age -- #end
文字列連結が書けない (文法が極めて限定的)
変数値が埋め込めない (プレースホルダを強制)
copyright 2015 kuwata-lab.com all rights reserved.©
なぜSQL Injectionが発生しないの?
// メインプログラム
<?php require_once 'sqltempl8.php'; $t = new SQLTempl8("sql/users.sql"); $vars = array('min_age'=>20); $pdo_conn = new PDO("....."); $pdo_stmt = $t->execute($pdo_conn, $vars); if ($pdo_stmt !== null) { foreach ($pdo_stmt as $row) { ... } }
SQL IDとしてテンプレートの ファイル名を指定 (生のSQLを指定しない)
copyright 2015 kuwata-lab.com all rights reserved.©
Q:PHPコードが埋め込めるのでは?セキュリティ上の大きな穴になりそう…// SQL template select <?= $_GET['param'] ?> from users where true -- % if :max_age and age <= :max_age -- % end -- % if :min_age and age >= :min_age -- % end
copyright 2015 kuwata-lab.com all rights reserved.©
A:'<?' をエスケープしますちゃんとSQL Syntax Errorになってくれます// PHP code select <<?php ?>?= $_GET['param'] ?> from users where true <?php if ($var['max_age']) { ?> and age <= :max_age <?php } ?> <?php if ($var['min_age']) { ?> and age >= :min_age <?php } ?>
copyright 2015 kuwata-lab.com all rights reserved.©
Q:'=' と 'is null' との切り替えは?値が null なら '=' を 'is null' にしてほしい
select * from users where deleted_on = :deleted
// これ↓は面倒なのでいやだ select * from users -- #if :deleted where deleted_on = :deleted -- #else where deleted_on is null -- #end
$vars['deleted'] がnullなら'=' を 'is null' に自動的に変更 してほしい
copyright 2015 kuwata-lab.com all rights reserved.©
A:MySQLの '<=>' 演算子を使おうポスグレなら 'is not distinct from' 演算子
select * from users where deleted_on <=> :deleted
値が null なら deleted_on is null と同じ、 それ以外なら deleted_on = :deleted と同じ
注:本当なら、SQL構文を解析して '=' 演算子を置換することが望ましい。
copyright 2015 kuwata-lab.com all rights reserved.©
Q:where句の追加が面倒では?例:上下限値があるときだけwhere句を追加
select * from users -- #if :max_age or :min_age where true -- #if :max_age and age <= :max_age -- #end -- #if :min_age and age >= :min_age -- #end -- #end
この条件分岐が面倒!!
copyright 2015 kuwata-lab.com all rights reserved.©
A:DBの最適化機能に任せよう「where true」が残っても性能に影響はない
select * from users -- #if :max_age or :min_age where true -- #if :max_age and age <= :max_age -- #end -- #if :min_age and age >= :min_age -- #end -- #end
余分な 'where true' は DBのOptimizerが取り除いてくれる (実行計画を見れば確認可能)
copyright 2015 kuwata-lab.com all rights reserved.©
Q:やっぱり埋め込み式が欲しい!placeholderでは表名やカラム名が指定できない
select * from blog_entries_<?= $user ?> where deleted_at is null order by <?= $sortkey ?>
from :table とはできない
order by :sortkey とはできない
copyright 2015 kuwata-lab.com all rights reserved.©
A:制限つき埋め込み式を用意しました値として、英数字と '_' と '.' だけを許可// SQL template select * from blog_entries order by [=:sortkey=]
// PHP code select * from blog_entries order by <?php if (! preg_match('/^\w+(\.\w+)*$/', $vars['sortkey']) throw new SQLTemplateError("error"); echo $vars['sortkey']; ?>
テーブル名やカラム名で なければ例外を発生
copyright 2015 kuwata-lab.com all rights reserved.©
課題✓xxx in (...) のサポートがまだ
プレースホルダではうまく扱うのが難しく、仕様を考え中(できるならPDOのエスケープ機能 quote() を拡張したい)
✓foreach文の導入がまだ これもプレースホルダでは扱いが難しいが、いわゆるバルクインサートでは必須なので実装したい
✓PDOは全部文字列として扱ってしまう! バインド変数にデータ型を指定するよう変更予定
copyright 2015 kuwata-lab.com all rights reserved.©
ここまでのまとめ✓SQLテンプレートならSQLiが発生しない
文字列結合が書けないプレースホルダやエスケープを強制生SQLを指定できない
copyright 2015 kuwata-lab.com all rights reserved.©
SQL構文木とは?SQLを木構造で表したデータ
select
* where
=
id 123
from
books
copyright 2015 kuwata-lab.com all rights reserved.©
SQL構文木の組み立て方通常は専用のライブラリを使うか、…
$q = new Query(); $q->select('*') ->from('books') ->where('id', '=', 123); $book = $q->query();
copyright 2015 kuwata-lab.com all rights reserved.©
SQL構文木の組み立て方O/R Mapperを使う
class Book extends Entity { ... }; class Books extends Schema { ... };
$q = new Query(Books); $q->where(Books::id->eq(123)); $book = $q->select('*');
copyright 2015 kuwata-lab.com all rights reserved.©
SQL構文木の組み立て方演算子オーバーライドが使えると自然な記述に
// Ruby book = Book.where(:id == 123).first()
// Python book = db.query(Book) \ .filter(Book.id == 123) \ .first()
true/falseではなく 部分構文木を返す
参考:「演算子オーバーライドをDSLに活用する」でggr
copyright 2015 kuwata-lab.com all rights reserved.©
構文木 → SQL文字列'=' と 'is null' も、値に応じて自動的に変換
where
==
id 123
where
==
id null
where id = 123 where id is null
copyright 2015 kuwata-lab.com all rights reserved.©
なぜSQL Injectionを防げるの?文字列結合後にSQLをパースするからSQLiが発生
$id = $_GET['id']; $sql = "select * from books where id = '$id'";
// もし $id が "' or 1<>'" なら…⋯ select * from books where id = '' or 1<>''
意図しない構造に 変わってしまった!
copyright 2015 kuwata-lab.com all rights reserved.©
なぜSQL Injectionを防げるの?SQL構文木なら意図しない変更ができない
where
=
id ' or 1 <>'
$q->where( Books::id, '=', $_GET['id'] );
悪意ある値を 受けとっても…
木構造は影響を受けない
copyright 2015 kuwata-lab.com all rights reserved.©
本当は構文木のままDBに送信したいSQL文字列は直列化形式として本当に妥当なのか?
where
=
id 123
送信
(注)直列化:構造のあるデータをバイト列に変換すること。データを送信したりファイルに保存するときに必要。
データベース
変換select ... from ... where ... and ...
もっと改ざんされにくい 形式でもいいのでは?
SQL文字列構文木
copyright 2015 kuwata-lab.com all rights reserved.©
注意!それは木構造ではない!
// 一見、木構造を作ってるように見えるが…⋯ $q->where('id = ?', $_GET['id']);
// 実はそうではない $q->where($_GET['col'].' = ?', $_GET['id']);
SQLを文字列で指定できてしまうのでアウト
copyright 2015 kuwata-lab.com all rights reserved.©
課題✓動作速度は遅い
木構造の構築も、SQLへの変換も、動作コストが大きい
昨今のCPUパワーをもっと 安全性のために使ってほしい
copyright 2015 kuwata-lab.com all rights reserved.©
ここまでのまとめ✓SQL構文木を作るとSQLiが発生しない
悪意ある文字列を渡されてもSQL構造が変更されない
copyright 2015 kuwata-lab.com all rights reserved.©
まとめ✓動的SQLを作るのに文字列結合を避ける
かわりにSQLテンプレートやSQL構文木を使う
✓本質的にはAPIやインターフェースの欠陥 生のSQLを文字列で渡せてしまう → だからSQLを文字列で組み立ててしまう → だからSQL Injectionがなくならない
copyright 2015 kuwata-lab.com all rights reserved.©
補足✓型はSQLiの防止に役立つか?
Yes。型はValidationに役立つ、そしてバインド変数のValidationはSQLi防止に有効、なので型はSQLi防止に役立つ
✓型安全であればSQLiは防げるか? No。型安全でも文字列連結はできる、そして文字列連結を使うとSQLiが起こり得る、なので型安全でもSQLiが起こり得る
参考:https://twitter.com/ockeghem/status/650171306428596224 https://twitter.com/tanakh/status/650214648382291968 https://twitter.com/tanakh/status/650215363943116800
型安全だけではSQLiは防げない、もう一工夫必要
copyright 2015 kuwata-lab.com all rights reserved.©
参考資料 (SQLテンプレート)✓SQLTempl8
https://github.com/kwatch/sqltempl8
✓Doma - SQLテンプレートの仕組み http://www.slideshare.net/taedium/doma-sql
✓DBFlute入門 - 外だしSQLの基本 http://gihyo.jp/dev/feature/01/dbflute/0005
✓S2JDBC - SQLファイル http://s2container.seasar.org/2.4/ja/s2jdbc_manager_sqlfile.html
copyright 2015 kuwata-lab.com all rights reserved.©
参考資料 (SQL構文木)✓演算子オーバーライドをDSLに活用する
http://j.mp/slide_opdsl
✓O/R Mapperによるトラブルを未然に防ぐ http://j.mp/slide_orm2
✓O/Rマッパーを支える技術 http://j.mp/slide_orm1
copyright 2015 kuwata-lab.com all rights reserved.©
お客さまにおすすめの新刊があります徳丸浩のWebセキュリティ教室
2015-10-22 発売予定
日経BP社
1944円
Let's Tw
eet!
絶賛予約受付中!
copyright 2015 kuwata-lab.com all rights reserved.©
私的反省会✓今回の内容は、HTMLテンプレートではすでに知られていたことばかり 自動エスケープ機能や、木構造が安全なことなど
✓それをSQLへ応用するのが遅れたせいで不幸な判決を生み出してしまった HTMLテンプレートエンジン作者の一人としてお詫びしますごめんなさい