【sqlインジェクション対策】徳丸先生に怒られない、動的sqlの安全な組み立て方...

58
徳丸先生に怒られない 動的SQL安全な組み立て方 Makoto Kuwata http://www.kuwata-lab.com/ PHP Conference 2015 ver 1.1.0

Upload: kwatch

Post on 21-Jan-2017

20.600 views

Category:

Technology


0 download

TRANSCRIPT

徳丸先生に怒られない

動的SQLの 安全な組み立て方

Makoto Kuwata http://www.kuwata-lab.com/

PHP Conference 2015

ver 1.1.0

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カンファレンスなので :)

なぜSQL Injection が 発生するのか?

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.©

なぜSQL Injectionが発生するのか?✓プレースホルダを使ってないから?

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やインターフェースの欠陥 結果はセキュリティ被害だけど、原因はセキュリティとは違う

SQLテンプレート

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を指定できない

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!

絶賛予約受付中!

One More Thing...

copyright 2015 kuwata-lab.com all rights reserved.©

私的反省会✓今回の内容は、HTMLテンプレートではすでに知られていたことばかり 自動エスケープ機能や、木構造が安全なことなど

✓それをSQLへ応用するのが遅れたせいで不幸な判決を生み出してしまった HTMLテンプレートエンジン作者の一人としてお詫びしますごめんなさい

copyright 2015 kuwata-lab.com all rights reserved.©おしまい