SlideShare a Scribd company logo
徳丸先生に怒られない
動的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 Tweet!
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 Tweet!
絶賛予約受付中!
One More Thing...
copyright 2015 kuwata-lab.com all rights reserved.©
私的反省会
今回の内容は、HTMLテンプレートでは

すでに知られていたことばかり
自動エスケープ機能や、木構造が安全なことなど
それをSQLへ応用するのが遅れたせいで

不幸な判決を生み出してしまった
HTMLテンプレートエンジン作者の一人としてお詫びします

ごめんなさい
copyright 2015 kuwata-lab.com all rights reserved.©
おしまい

More Related Content

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

  • 2. copyright 2015 kuwata-lab.com all rights reserved.© 本発表の作成には徳丸先生は関わって
 おりません。 内容の責任はすべて発表者にあるので、 ご意見がありましたら先生にではなく 発表者までお願いします。 !! CAUTION !!
  • 3. copyright 2015 kuwata-lab.com all rights reserved.© 発表の背景:裁判と損害賠償 引用: http://blog.tokumaru.org/2015/01/sql.html SQLインジェクション脆弱性が原因でクレジット カード情報が漏洩した事件につき、ショップ側が
 開発会社を相手取り損害賠償請求の裁判を起こし、 ショップ側が勝訴 結果、3131万9568円の損害を認定し、その3割を
 控除して、2262万3697円の損害賠償をY社に
 命じた
  • 4. copyright 2015 kuwata-lab.com all rights reserved.© 発表の背景:ばく大な金額 引用: http://www.ipa.go.jp/files/000013778.pdf (SQLiによる推計される被害額について) 推計すると、約 4,800 万円∼1 億円程度を要する ものと考えられる。
  • 5. copyright 2015 kuwata-lab.com all rights reserved.© 発表の目的 SQL Injection が起きる真の原因を考える 「プレースホルダを使ってないから」では考察不足 SQL Injection を起こさない方法を考える 「プレースホルダを使え」では対策不足
  • 6. copyright 2015 kuwata-lab.com all rights reserved.© アジェンダ 考察:なぜSQL Injectionが発生するのか? 対策:SQLテンプレート 対策:SQL構文木 まとめ
  • 7. copyright 2015 kuwata-lab.com all rights reserved.© 前提知識 SQL Injection が何か、知ってること (以降では SQL Injection を SQLi と省略) プレースホルダやバインド変数について
 知ってること 「:id」や「?」がプレースホルダ、渡す値がバインド変数 PHPのコードが読めること PHPカンファレンスなので :)
  • 9. copyright 2015 kuwata-lab.com all rights reserved.© SQL Injection 対策 エスケープしろ!プレースホルダを使え! http://www.ipa.go.jp/files/000017320.pdf
  • 10. copyright 2015 kuwata-lab.com all rights reserved.© なぜSQL Injectionが発生するのか? プレースホルダを使ってないから?
  • 11. 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!
  • 12. copyright 2015 kuwata-lab.com all rights reserved.© なぜSQL Injectionが発生するのか? プレースホルダを使ってないから? 実情は、プレースホルダつきの安全なSQLを組み立てて
 いる最中に SQL Injection が入り込んでいる   これこそが真の原因 文字列連結でSQLを組み立ててるから
  • 13. copyright 2015 kuwata-lab.com all rights reserved.© 変数の埋め込みも文字列連結と同じ // これと $sql .= " and age >= ".$min_age; // これは同じ $sql .= " and age >= $min_age"; 変数の埋め込みも 文字列連結と同じ
  • 14. copyright 2015 kuwata-lab.com all rights reserved.© なぜ文字列結合でSQLを作るのか? プレースホルダではできないことがあるから 要素数が動的に変わる in (....) 、order by でのカラム名指定   真の原因その2 // SQLを文字列で渡せてしまえるんだから、 // SQLを文字列結合で組み立ててしまうのは当然のこと $st = pdo->prepare("select * from users"); SQLを文字列で指定できてしまうから
  • 15. copyright 2015 kuwata-lab.com all rights reserved.© SQL Injection を防ぐには? 文字列連結ではない方法で動的SQLを
 組み立てればよい 方法:SQLテンプレート or SQL構文木 (詳細は後述) SQL文字列ではない方法でSQLを
 指定できるようにすればよい 方法:SQL ID or SQL構文木 (詳細は後述)
  • 16. copyright 2015 kuwata-lab.com all rights reserved.© SQL Injection が発生する本当の原因は、 ライブラリや API のインターフェースに 欠陥があるからである。 間違いを誘発するものを使い続ける限り、 SQL Injection がなくなることはない。 Let's Tweet!
  • 17. copyright 2015 kuwata-lab.com all rights reserved.© ここまでのまとめ SQLを文字列連結で組み立てるから、
 SQL Injection が発生する 対策:文字列連結ではない方法でSQLを組み立てる 生のSQLを文字列で渡せてしまうから、
 文字列連結を使ってしまう 対策:生のSQLを渡すかわりに別の方法でSQLを指定する 本質的にはAPIやインターフェースの欠陥 結果はセキュリティ被害だけど、原因はセキュリティとは違う
  • 19. copyright 2015 kuwata-lab.com all rights reserved.© SQLテンプレートとは? SQLを生成するためのテンプレート WebアプリでのHTMLテンプレートと同じ SQL Injection が発生しない(原理上は) 文字列連結が不可、値のエスケープ or プレースホルダを強制 注:内部の実装では文字列連結を使うが、開発者が明示的に使うことはできない。
  • 20. 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
  • 21. 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 } ?> 注:実際はもう少し複雑なコードに変換される
  • 22. 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
  • 23. copyright 2015 kuwata-lab.com all rights reserved.© SQLTempl8 SQLを対象としたテンプレートエンジン https://github.com/kwatch/sqltempl8/ まだコンセプト実装 (いろいろ足りない) 使い方は随時変わるので、最新のドキュメントを参照のこと
  • 24. 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
  • 25. 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: }
  • 26. 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 文字列連結が書けない (文法が極めて限定的) 変数値が埋め込めない (プレースホルダを強制)
  • 27. 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を指定しない)
  • 28. 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
  • 29. 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 } ?>
  • 30. 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' に自動的に変更 してほしい
  • 31. 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構文を解析して '=' 演算子を置換することが望ましい。
  • 32. 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 この条件分岐が面倒!!
  • 33. 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が
 取り除いてくれる (実行計画を見れば確認可能)
  • 34. 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 とはできない
  • 35. 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']; ?> テーブル名やカラム名で なければ例外を発生
  • 36. copyright 2015 kuwata-lab.com all rights reserved.© 課題 xxx in (...) のサポートがまだ プレースホルダではうまく扱うのが難しく、仕様を考え中
 (できるならPDOのエスケープ機能 quote() を拡張したい) foreach文の導入がまだ これもプレースホルダでは扱いが難しいが、いわゆる
 バルクインサートでは必須なので実装したい PDOは全部文字列として扱ってしまう! バインド変数にデータ型を指定するよう変更予定
  • 37. copyright 2015 kuwata-lab.com all rights reserved.© ここまでのまとめ SQLテンプレートならSQLiが発生しない 文字列結合が書けない
 プレースホルダやエスケープを強制
 生SQLを指定できない
  • 39. copyright 2015 kuwata-lab.com all rights reserved.© SQL構文木とは? SQLを木構造で表したデータ select * where = id 123 from books
  • 40. copyright 2015 kuwata-lab.com all rights reserved.© SQL構文木の組み立て方 通常は専用のライブラリを使うか、… $q = new Query(); $q->select('*') ->from('books') ->where('id', '=', 123); $book = $q->query();
  • 41. 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('*');
  • 42. 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
  • 43. copyright 2015 kuwata-lab.com all rights reserved.© 構文木 → SQL文字列 '=' と 'is null' も、値に応じて自動的に変換 where == id 123 where == id null where id = 123 where id is null
  • 44. 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<>'' 意図しない構造に 変わってしまった!
  • 45. copyright 2015 kuwata-lab.com all rights reserved.© なぜSQL Injectionを防げるの? SQL構文木なら意図しない変更ができない where = id ' or 1 <>' $q->where( Books::id, '=', $_GET['id'] ); 悪意ある値を 受けとっても… 木構造は影響を 受けない
  • 46. copyright 2015 kuwata-lab.com all rights reserved.© 本当は構文木のままDBに送信したい SQL文字列は直列化形式として本当に妥当なのか? where = id 123 送信 (注)直列化:構造のあるデータをバイト列に変換すること。
 データを送信したりファイルに保存するときに必要。 データベース 変換 select ... from ... where ... and ... もっと改ざんされにくい 形式でもいいのでは? SQL文字列構文木
  • 47. copyright 2015 kuwata-lab.com all rights reserved.© 注意!それは木構造ではない! // 一見、木構造を作ってるように見えるが…⋯ $q->where('id = ?', $_GET['id']); // 実はそうではない $q->where($_GET['col'].' = ?', $_GET['id']); SQLを文字列で指定できて しまうのでアウト
  • 48. copyright 2015 kuwata-lab.com all rights reserved.© 課題 動作速度は遅い 木構造の構築も、SQLへの変換も、動作コストが大きい 昨今のCPUパワーをもっと 安全性のために使ってほしい
  • 49. copyright 2015 kuwata-lab.com all rights reserved.© ここまでのまとめ SQL構文木を作るとSQLiが発生しない 悪意ある文字列を渡されてもSQL構造が変更されない
  • 51. copyright 2015 kuwata-lab.com all rights reserved.© まとめ 動的SQLを作るのに文字列結合を避ける かわりにSQLテンプレートやSQL構文木を使う 本質的にはAPIやインターフェースの欠陥 生のSQLを文字列で渡せてしまう
 → だからSQLを文字列で組み立ててしまう
 → だからSQL Injectionがなくならない
  • 52. 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は防げない、 もう一工夫必要
  • 53. 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
  • 54. 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
  • 55. copyright 2015 kuwata-lab.com all rights reserved.© お客さまにおすすめの新刊があります 徳丸浩のWebセキュリティ教室 2015-10-22 発売予定 日経BP社 1944円 Let's Tweet! 絶賛予約受付中!
  • 57. copyright 2015 kuwata-lab.com all rights reserved.© 私的反省会 今回の内容は、HTMLテンプレートでは
 すでに知られていたことばかり 自動エスケープ機能や、木構造が安全なことなど それをSQLへ応用するのが遅れたせいで
 不幸な判決を生み出してしまった HTMLテンプレートエンジン作者の一人としてお詫びします
 ごめんなさい
  • 58. copyright 2015 kuwata-lab.com all rights reserved.© おしまい