全てのセキュリティ対策は緩和策だと考えるべきです。これは個々の対策が完全であるか検証することが容易ではないからです。例えば、SQLインジェクション1つとっても本当に完全であるか?検証することは容易ではありません。プログラムが本当に思っているように動作するのか?検証する研究は、まだまだ研究段階です。
しかし、容易ではないからといって諦める訳にもいきません。不完全であっても形式的な論理検証は容易にできます。
論理的な正しさを検証する方法
必要十分条件を使って考えると、使わない場合に較べ誤りを少なくできます。プリペアードクエリがSQLインジェクション対策として必要十分であるか考えてみます。SQLインジェクションはSQL文に変数が含まれる”動的クエリ”の場合に発生することを頭に入れておきます。
- p – プリペアードクエリを使い全てのパラメーターをSQL文とパラメーターに分離する
- q – SQLインジェクションができない
とした場合、
- p ⇒ q(pならばq)
- q ⇒ p(qならばp)
は成り立ちますか?
残念ながら成り立ちません。反例が1つでもあれば、これらの条件が成り立たないことを証明できます。
「プリペアードクエリを使い全てのパラメーターをSQL文とパラメーターに分離する」ならば「SQLインジェクションができない」に対する反例を考えてみます。
- テーブル名やカラム名など識別子が変数
- ASCやDESCなどのSQL語句が変数
簡単に「プリペアードクエリを使い全てのパラメーターをSQL文とパラメーターに分離する」ならば「SQLインジェクションができない」は論理的に成り立たないことが解ります。反対のq ⇒ pを考えるまでもありません。
これらの条件を成り立たせるには前提条件が必要です。(太字が追加条件)
- プリペア文に変数が一切ないとする(プリペア文は常に静的SQL)
- p ⇒ q(pならばq)
- q ⇒ p(qならばp)
これなら成り立ちます。
論理的な正しさが証明できても、前提条件が正しいか?疑ってみる(検証する)ことが重要です。
プリペア文に変数が一切ないとする前提は妥当な前提なのか?
前提条件を付けて成り立つようにしても、それが実際に妥当(現実的)でなければ意味がありません。
- プリペア文に変数が一切ないとする(プリペア文は常に静的SQL)
この条件を満たすには
- SELECT $i_need_this_col FROM tbl;
と言った、テーブル名(識別子)を変数とし特定のカラムを抽出するクエリは書けなくなります。ASC/DESCといったSQL語句は変数にしない、とするとプリペア文を複数作ることになります。
- SELECT * FROM tbl ORDER BY col ASC
- SELECT * FROM tbl ORDER BY col DESC
ほぼ同じSQLクエリを2つ作る無駄をしなければなりません。colを変数にして
- SELECT * FROM tbl ORDER BY $sort_by_this_col DESC
特定のカラムでソートする事もできません。これを静的SQLだけにするには2つどころではない、ほぼ同じプリペア文を大量に作る必要があります。1:
複雑なクエリの場合、サブクエリなどを使う場合があります。この場合、プリペア文全体を静的な文字列(プログラムのコードとして埋込んだ文字列)としてコードに書くことが困難な場合があります。
- SELECT * FROM tbl WHERE id IN ($subquery);
などと書く必要があります。このケースは簡単なSQLなので2つに分けて実行して、IN条件にプレイスホルダを書いてプリペアードクエリにすることも可能です。しかし、いつもSQLクエリを分割して実行する方法が最適(やり易い、効率的)とは限りません。
より現実的な前提を考える
”識別子やSQL語句が変数になる場合”を想定しなければならないことが解りました。プリペアードクエリはパラメーターしか分離できないので、別の方法が必要であることが解ります。
- 識別子 – 識別子用のエスケープを行う
- SQL語句 – 値が妥当か検証する
- サブクエリなど – 動的SQLとして安全対策を行う = パラメーターエスケープ+上記の識別子とSQL語句対策(文字列で組み立てる動的SQLなので、動的SQLと同じ対策が必要)
SQLインジェクション対策には、これらの対策が必要であることが判りました。
これらの対策を追加すると(太字が追加条件)
- 識別子 – 識別子用のエスケープを行う
- SQL語句 – 値が妥当か検証する
- サブクエリなど – 動的SQLとして安全対策を行う = パラメーターエスケープ+上記の識別子とSQL語句対策
プリペア文に変数が一切ないとする(プリペア文は常に静的SQL)無効な条件なので削除- p ⇒ q(pならばq)
- q ⇒ p(qならばp)
となり、これは成り立ちそうです。反例がないか考えてみます。
大丈夫そうです。完全にSQLインジェクション対策をするには
- パラメーターのエスケープ
- 識別子のエスケープ
- SQL語句のバリデーション
これらが必要なことが判りました。
これで大丈夫!として終わらせると問題が残る場合があります。
そもそもの条件を疑ってみる
論理的な正しさが証明できても、そもそもの条件
- p – プリペアードクエリを使い全てのパラメーターをSQL文とパラメーターに分離する
- q – SQLインジェクションができない
が正しいか?疑ってみる(検証する)ことが重要です。
出力のセキュリティ対策の目的は「外部システムを誤作動させない」ことにあります。外部システムの誤作動はインジェクションによって行われます。qをSQLに限定しない”インジェクション”に換えて考えてみます。
- p – プリペアードクエリを使い全てのパラメーターをSQL文とパラメーターに分離する
- q – インジェクションができない
この場合に
- 識別子 – 識別子用のエスケープを行う
- SQL語句 – 値が妥当か検証する
- サブクエリなど – 動的SQLとして安全対策を行う = パラメーターエスケープ+上記の識別子とSQL語句対策
- p ⇒ q(pならばq)
- q ⇒ p(qならばp)
が成り立つか考えます。
テキストインタフェース処理の基本を理解していれば「全てのテキストインターフェースはインジェクションに脆弱であると考えるべき」であることを解っています。SQLに別のテキストインターフェースがないか、考えてみます。
- LIKEクエリ
- 正規表現
- JSON
- XML
等があることが判ります。出力先のパラメーターコンテクストに別のコンテクストがあることになります。それぞれのコンテクスト(テキストインターフェース)に対して、適切なインジェクション対策が必要です。(ここでは、これらの対策は省略)
ここまでで必要となった条件(太字が追加条件)
- 識別子 – 識別子用のエスケープを行う
- SQL語句 – 値が妥当か検証する
- サブクエリなど – 動的SQLとして安全対策を行う = パラメーターエスケープ+上記の識別子とSQL語句対策
- パラメーターにサブコンテクストがある場合、それぞれ適切な出力対策を適用(LIKEエスケープ、正規表現エスケープ、JSONエスケープ、XMLエスケープ/XPathエスケープなど)
- p ⇒ q(pならばq)
- q ⇒ p(qならばp)
これで十分でしょうか?
値そのものに好ましくない物があるか?
ここまでで、誤作動はほぼ防げるようになりました。しかし、これで十分か、値自体が好ましくない動作をする物がないか考える必要があります。例えば、
- SELECT * FROM tbl LIMIT 999999999999999;
です。他にもデータベースに受け入れられないデータがあると困ります。
- データの形式的正しさの検証(入力処理 – 入力バリデーション)
- データの仕様的/論理的正しさの検証(ロジック処理)
これらが実施されている必要があります。
ここまでで必要となった条件(太字が追加条件)
- データの形式的正しさの検証(入力処理 – 入力バリデーション)
- データの仕様的/論理的正しさの検証(ロジック処理)
- 識別子 – 識別子用のエスケープを行う
- SQL語句 – 値が妥当か検証する
- サブクエリなど – 動的SQLとして安全対策を行う = パラメーターエスケープ+上記の識別子とSQL語句対策
- パラメーターにサブコンテクストがある場合、それぞれ適切な出力対策を適用(LIKEエスケープ、正規表現エスケープ、JSONエスケープ、XMLエスケープ/XPathエスケープなど)
- p ⇒ q(pならばq)
- q ⇒ p(qならばp)
これなら大丈夫そうです。
まとめ
論理的に考えると必要なモノが見えてきます。”SQLインジェクション対策だけ”を考えるなら、
- 識別子 – 識別子用のエスケープを行う
- SQL語句 – 値が妥当か検証する
- サブクエリなど – 動的SQLとして安全対策を行う = パラメーターエスケープ+上記の識別子とSQL語句対策
- p ⇒ q(pならばq)
- q ⇒ p(qならばp)
ここまででも大丈夫です。しかし、”SQLインジェクション対策”はセキュリティ対策の”手段”であって”目的”ではありません。目的はインジェクション攻撃などの、”あらゆるリスクを許容範囲内に抑えること”にあります。基本的に全てのインジェクション攻撃は防止できる必要があります。
そこで、以下のような条件が必要であることが判りました。(太字が追加条件)
- データの形式的正しさの検証(入力処理 – 入力バリデーション)
- データの仕様的/論理的正しさの検証(ロジック処理)
- 識別子 – 識別子用のエスケープを行う
- SQL語句 – 値が妥当か検証する
- サブクエリなど – 動的SQLとして安全対策を行う = パラメーターエスケープ+上記の識別子とSQL語句対策
- パラメーターにサブコンテクストがある場合、それぞれ適切な出力対策を適用(LIKEエスケープ、正規表現エスケープ、JSONエスケープ、XMLエスケープ/XPathエスケープなど)
- p ⇒ q(pならばq)
- q ⇒ p(qならばp)
これなら論理的に成り立ちそうです。
しかし、これでも完璧ではないかも知れません。疑ってみることが必要です。この疑うプロセスはセキュリティ標準では「定期的なレビュー」として組み込まれています。
「プリペアードクエリをだけ使う」では出力対策の目的である「外部システムを誤作動させない」に対して、論理的に全く不十分な対策であることが理解りました。
最後に追加した、以下の前提条件はSQLインジェクション対策に限らず、基本的には全てのインジェクション対策に必要です。
- データの形式的正しさの検証(入力処理 – 入力バリデーション)
- データの仕様的/論理的正しさの検証(ロジック処理)
入力バリデーションはソフトウェアの正常動作を保証する機能として欠かせません。これがセキュアコーディングやセキュリティガイドラインで「入力バリデーション」を第一の対策としている理由です。入力バリデーションは論理的に必要かつ最初に行っておくべき事、さらにリスク廃除/削減効果が高いので第一のセキュリティ対策とするのが妥当です。
今回は私がどのようにセキュリティ対策を考えているのか紹介しました。セキュリティでなくても、こうやってシステムの仕様などを頭の中で考えていると思います。
一番難しいのは、漏れ無く考える、です。こうやって考えていても、時々はたとえ解っていることであっても、前提となる条件をついつい見落としたりします。論理的に成り立っているかどうか?は反証1つで成り立たないことが証明できます。反証がある場合は注意が必要です。2 反証とならないよう前提条件を加えるだけ、では上手くいかない場合があります。ここの例だと
- プリペア文に変数が一切ないとする(プリペア文は常に静的SQL)
この前提条件は現実には使い物にならない前提であることが理解ります。論理的に漏れ無く考えれば理解ることですが、このエントリを書いた時には「プリペアードクエリだけ十分!」とする誤解がとても多かったようです。
繰り返しになりますが「一番難しいのは、漏れ無く考える」です。エスケープ方法1つとっても、この10年で「漏れ無く考える」が出来ていない為にやり方や前提条件が変わってきました。そういうモノだと理解して、より良い方法で対応する、と良いと思います。
変わっていないモノは原則や概念です。セキュアコーディングの原則や概念(昔は防御的プログラミングと呼ばれた)は20年前、30年前、もっと前から変わっていません。原則や概念を正しく理解していると間違いが少くなります。3
参考:
- ASC, DESCがありソート対象の平均カラム数、テーブル数、ジョインした結果数分が必要なので、およそ 「2 × 平均カラム数 × (テーブル数 + ジョインした結果数)」のプリペア文が必要になります。簡単に何十ものほぼ同じプリペア文が必要になります。変数を使えば1つで済みます。 ↩
- 出鱈目な条件の反証には意味がないことも知っておく必要があります。例えば「入力文字コードがEBCDICで来るかも知れない!だから文字エンコーディングはバリデーションできない!」などです。馬鹿馬鹿しいと思うかも知れませんが、これは実際に議論中に出てきた反証です。 ↩
- 原則であっても時々、原則でなくなるモノがあります。例えば、同じまたは同じような処理は一箇所にまとめる、は昔は原則でしたが現在は原則とは考えられていません。例えば、セキュリティ対策では「同じまたは同じような処理」を多層防御として実装する必要があります。 ↩