セキュアコーディング原則において、インジェクション対策の為に重要な原則は
- 原則1: 全ての入力をバリデーションする
- 原則7: 全ての出力を無害化する
の2つです。これらに、一般的なプログラミング原則であるフェイルファースト原則とフェイルセーフ原則、ゼロトラストを適用するとセキュアコーディングになります。
簡単なSQLインジェクション対策コードを使ってセキュアコーディングの概念を紹介します。
セキュアコーディング/セキュアプログラミングの原則と技術は国際情報セキュリティ標準(ISO 27000)でも要求される技術です。しかし、根本から誤ったセキュリティ対策の概念が長年啓蒙されています。GDPR対策にもISO 27000は重要です。日本に於てもISO 27000が要求する基礎的対策ができていない場合、法的リスクが非常に高いと言わざるを得ません。
危険なSQLインジェクション対策
まずよく見かける危険なSQLインジェクション対策を紹介します。
- プリペアードクエリ/プレイスホルダを使う
これ”だけ”では危険なSQLインジェクション対策です。安全ではありません。対策として間違ってはいないのですが、これ”だけ”だと穴だらけです。
テーブル(tbl)から特定のカラム($_GET[‘col’])を条件($_GET[‘pettern’])に一致するレコードを指定した数($_GET[‘limit’])だけ抽出するクエリを考えます。
pg_query_params(
'SELECT id, ' .$_GET['col'] .' FROM tbl WHERE name ~ $1 LIMIT $2;',
[$_GET['pattern'], $_GET['limit']]);
pg_query_params()はPHPのプリペアードクエリを実行するPostgreSQL関数です。この文はプリペアードクエリを使っていますが、これだけでは非常に危険です。
ユーザー入力の
- 抽出カラム名($_GET[‘col’])が直接クエリに埋め込まれている (SQLインジェクションが可能で致命的)
- nameカラム正規表現マッチパターン($_GET[‘pattern’])がプリペアードクエリのパラメーターとして直接渡されている(正規表現インジェクション、ReDoS攻撃など、が可能で致命的)
- 抽出レコード数($_GET[‘limit’])がプリペアードクエリのパラメーターとして直接渡されている(物量によるDoS攻撃が可能で致命的)
「SQLインジェクション対策はプリペアードクエリだけを使っていれば完璧!」「クエリを実行する際に対策するだけで十分!」と聞いた事がある開発者は少なくないと思います。実はこれが大間違いです。
ダメなSQLインジェクション対策である理由
アプリケーションソフトウェアの場合、出力対策(SQL命令を作る部分)だけで対策しても、セキュリティ対策としてあまり意味がありません。
全く意味がないのではありませんが、インジェクション攻撃対策には完全性が求められるので、インジェクション攻撃を完全に防げない防御方法ではダメです。
識別子
- 抽出カラム名($_GET[‘col’])が直接クエリに埋め込まれている (SQLインジェクションが可能で致命的)
抽出カラム名($_GET[‘col’])の埋め込み問題を解決するには識別子(カラム名)をエスケープすれば”SQL命令のインジェクションは防止できます”。PostgreSQLには識別子をエスケープするC APIのPQescapeIdentifier()が定義されており、PHPではpg_escape_identifier()として利用できます。利用すると以下のようになります。
pg_query_params(
'SELECT id, ' .pg_escape_identifier($_GET['col']) .' FROM tbl WHERE name ~ $1 LIMIT $2;',
[$_GET['pattern'], $_GET['limit']]);
DBアクセス抽象化ライブラリなどならこれで十分です。 $_GET[‘col’]に
- ”name”; SELECT * FROM secret_table; –“
- “secret AS name”
などとSQLインジェクション攻撃用の文字列を設定されても、$_GET[‘col’]によりSQL命令のインジェクションはできません。
しかし、アプリケーションでは不十分な対策です。”SQL命令のインジェクションは防止できます”が、”攻撃者による任意のカラムデータ抽出”は防げません。
重要: どのカラムを利用するのか?設定できるのはアプリケーションです。ライブラリには設定できません。
例えば、tblがユーザー情報テーブルだとして暗号などに利用するマスター鍵がsecretなどとして保存されている場合に、攻撃者が$_GET[‘col’] に
- “secret”
を設定するとマスター鍵情報を盗まれる可能性があります。存在しないカラムを設定された場合に”SQL実行エラーが起きることが問題の原因”になる可能性もあります。一般にSQL実行エラーはフェイルファースト原則違反の”遅すぎるエラー”だからです。
以下のようなクエリをアプリケーションで書くことは無いとは思いますが、、、
バリデーション無しでは自由自在にデータを盗めるクエリの実行例:
pg_query_params(
'SELECT '. $_GET['col'] . ' FROM '. $_GET['tbl'] .' WHERE id = $1',
[$_GET['id']]);
バリデーション無しでは自由自在にデータを改ざんできるクエリの実行例:
pg_query_params(
'UPDATE '. $_GET['tbl'] . ' SET '. $_GET['col'] .' = $1 WHERE id = $2',
[$_GET['val'], $_GET['id']]);
アプリケーションではほぼ見なくても、抽象化ライブラリなどではこれに近いコードは割と見かけます。ライブラリは”汎用的”に作られているからです。
アプリケーションでよく見かけるコードなら、、、
SQLインジェクションでやりたい放題できるプリペアードクエリ:
pg_query_params(
'SELECT col1, col2 FROM tbl WHERE category = $1 ORDER BY '. $_GET['sort_col'],
[$_GET['category']]);
といったコードがあります。SELECTしたレコードを並べ替える処理はアプリケーションでも普通にあります。
SQLインジェクション対策(というよりインジェクション対策全て)を説明する上で、データバリデーションは欠かすことが出来ないセキュリティ対策です。CERT、OWASPなどでは正しく「バリデーションすべし」と解説していますが、解説が省略されているどころか、敢えて無視すべき、とする出鱈目な解説さえまかり通っているのが現状です。
パラメーター – 正規表現
- nameカラム正規表現マッチパターン($_GET[‘pattern’])がプリペアードクエリのパラメーターとして直接渡されている(正規表現インジェクション – DoS攻撃など、が可能で致命的)
pg_query_params(
'SELECT id, ' .$_GET['col'] .' FROM tbl WHERE name ~ $1 LIMIT $2;',
[$_GET['pattern'], $_GET['limit']]);
name ~ $1、$_GET[‘pattern’]任意の正規表現マッチが可能です。取り上げているクエリでは、そもそも正規表現検索を行う仕様としているので、正規表現のメタ文字を全てエスケープする、といった対策も取れません。(ワイルドカード検索ならLIKEクエリが使えますが、ここでは正規表現検索が問題となる例として正規表現を使っています)
任意の正規表現が指定可能な状態で、正規表現インジェクションによるReDoSを防止するのは不可能だと考えるべきです。ここではReDoSの解説を省略します。以下のブログと関連ブログを参考にしてください。
パラメーター – 値そのモノ
- 抽出レコード数($_GET[‘limit’])がプリペアードクエリのパラメーターとして直接渡されている(物量によるDoS攻撃が可能で致命的)
pg_query_params(
'SELECT id, ' .$_GET['col'] .' FROM tbl WHERE name ~ $1 LIMIT $2;',
[$_GET['pattern'], $_GET['limit']]);
LIMIT $2、$_GET[‘limit’]が返すレコード数を制限する部分ですが、プリペアードクエリを使うだけ、では意味がありません。
100万レコードを抽出してしまうようなクエリを繰り返しリクエストしても何の問題もないシステムはそうはありません。
これだけでは数値以外の出鱈目なデータによる、遅すぎるクエリエラーも問題の原因になります。このクエリはSELECT文なのでまだ良いですが、UPDATE文で出鱈目なデータを保存してしまうと問題は更に深刻です。データベースに保存された出鱈目なデータがどこでどこのような問題を起こすか、予測は困難です。
セキュアコーディングによる安全なSQLクエリの実行
セキュアコーディング/セキュアプログラミングの概念を取り入れると「出力対策だけでSQLインジェクション対策をしよう!」とは考えなくなります。
- 原則1: 全ての入力データをバリデーションする
- 原則7: 全ての出力を無害化する
これに加えてシステム開発/プログラミングの一般原則である
- ゼロトラスト
- フェイルファースト ※1 参考: 形式的検証と組み合わせ爆発
- フェイルセーフ ※2
で考えると自動的(=論理的)に安全なSQLクエリの実行方法が導けます。
- アプリケーションの入力データ処理でできる限り厳格な形式的なデータバリデーションを行う
- アプリケーションロジックでできる限り厳格な論理データバリデーションを行う
- アプリケーションの出力データ処理でできる限り無害化を行う
※1 フェイルファースト: 失敗するモノはできる限り早く失敗させる原則
※2 フェイルセーフ: 他の対策にミス/漏れがある場合でもできる限り安全に失敗させる原則(≒多層防御)
入力データ処理 – 原則: 全ての入力データをバリデーションする
- アプリケーションの入力データ処理でできる限り厳格な形式的なデータバリデーションを行う
セキュアコーディング原則には含まれていませんが、それ以前に利用すべき一般原則であるゼロトラスト(≒ホワイトリスト)とフェイルファースト原則も含めて入力データ処理は行います。
厳格に処理するコード例を簡単にする為、PHP用のバリデーションライブラリのValidate for PHPを使ったサンプルコードを使います。
識別子のバリデーション
適切に構造化された一般的なアプリケーションであれば、アプリケーションのエントリポイント(≒MVCのコントローラー)で受け入れ可能な識別子を設定できます。
フェイルファースト原則に従いできる限り早くバリデーションできる箇所はMVCアーキテクチャーならコントローラーになります。一般にSQLクエリ識別子のバリデーションはコントローラーで可能です。
$_GET[‘col’]のバリデーション例:
$col_spec = [
VALIDATE_STRING, // 文字列としてバリデーション
VALIDATE_FLAG_NONE, // 配列による完全一致なのでフラグ必要無し
[
// そもそも長さがおかしい文字列データはエラー。バリデーションの最適化。
'min' => 4, 'max' => 10,
// 許可する文字列データの配列
'values' = [
'name' => true,
'first_name' => true,
'last_name' => true,
],
],
];
// バリデーションエラーで例外。エラー処理は例外に任せる。
$col = validate($ctx, $_GET['col'], $col_spec);
アプリケーションロジックで許可する識別子を更に絞り込める場合は、アプリケーションロジックでもバリデーションを行います。一般的なアプリケーションではあまりありませんが、識別子の「ユーザー入力エラー」が発生する仕様の場合、コントローラーでの形式的バリデーションに加え、モデルでのバリデーションを行い適切なエラー処理を行います。(フェイルファーストを忘れずに!コントローラーでのバリデーションは基本全ての入力データに対して行います)
正規表現のバリデーション
アプリケーション仕様として許可する正規表現検索を定義する必要があります。その仕様自体が脆弱だと問題があるので脆弱な仕様でなくてはなりません。ここでは”.”, “*”だけ認める仕様とします。”.”,”*”だけならReDoS攻撃は行なえません。
$pattern_spec = [
VALIDATE_STRING, // 文字列としてバリデーション
VALIDATE_STRING_ALPHA | VALIDATE_STRING_SPACE,
[
// クライアント側のバリデーション実施が前提。そもそも長さがおかしい文字列データはエラー。バリデーションの最適化。
'min' => 4, 'max' => 20,
'ascii' = '.*',
],
];
// バリデーションエラーで例外。エラー処理は例外に任せる。
$pattern = validate($ctx, $_GET['pattern'], $patten_spec);
クエリパラメーター値のバリデーション
全ての入力データはバリデーションされなければならないので、LIMIT句のパラメーターのバリデーションも必要です。(先の正規表現もクエリパラメーター)
LIMIT句は正の整数なので整数としてバリデーションします。※ Web環境では基本パラメーターは文字列として渡されます。攻撃者はどんな文字列でも送れます。
$limit_spec = [
VALIDATE_INT, // 文字列としてバリデーション
VALIDATE_FLAG_NONE, // オプションフラグ無し
[
// クライアント側のバリデーション実施が前提。そもそも長さがおかしい文字列データはエラー。バリデーションの最適化。
'min' => 0, 'max' => 1000,
],
];
// バリデーションエラーで例外。エラー処理は例外に任せる。
$limit = validate($ctx, $_GET['limit'], $limit_spec);
出力データ処理 – 原則: 全ての出力を無害化する
出力データを外部出力先や複雑な処理(正規表現やXML処理など)に渡す場合、全ての出力データは無害化されていなければなりません。
先に解説した通り”ての出力データの無害化”は出力対策だけでは不可能です。入力データ対策によるバリデーションが欠かせません。
出力対策で注意しなければならない点は
- 入力対策と出力対策は独立した対策
である事です。要するにセキュアコーディング/セキュアプログラミングに準拠するプログラムなら”両方実施しなければならないモノ”です。 ※ セキュアコーディング/セキュアプログラミングはISO 27000の要求事項
理想的には出力コードで「全ての出力を無害化する」ことが出来れば良いのですが、現実的には難しい場合も多いです。この場合、入力データバリデーションが妥当であることを前提に
- 出力対策の無害化はフェイルセーフ対策に留める
とする設計も許容範囲です。これによりソフトウェアの設計者にはある程度の自由度が生まれます。
識別子の無害化
入力処理で厳格にデータバリデーションしていれば、SQLクエリでユーザー入力の識別子を使っても問題ありません。しかし、「入力対策と出力対策は独立した対策」なので、よほど単純なアプリでない限り、出力時にも識別子の無害化が欠かせません。
SQL識別子の無害化はSQLクエリパラメーターの無害化と異なります。許容可能なレベルに無害化するにはバリデーションが必要です。
- 想定外の識別子を指定されると困る ← データ漏洩/改ざん/DoSの原因
DBアクセス抽象化ライブラリなら識別子の無害化は”識別子エスケープ”で十分です。大半のデータベースは識別子をクオートする事により、
- SQL予約語の識別子
- 半角スペースを含む識別子
- マルチバイト文字を含む識別子
などが利用可能になります。できる限りデータベースの機能を使える様に設計するDBアクセス抽象化ライブラリでは可能な限り汎用的に利用できるようにするのが一般的です。”識別子エスケープだけ”で発生する問題に対策する責任はライブラリにはありません。
これに反して、専用ソフトウェアであるアプリケーションは可能な限り限定的に識別子を許可する必要があります。「想定外の識別子を指定されると困る」からです。
開発者にはセキュリティ設計上の自由があり、出力時のバリデーションには以下のオプションを選べます。
- 厳格に利用を許可する識別子名をバリデーションする
- 利用を許可する文字種だけバリデーションする (入力データバリデーションが妥当であることが前提。フェイルセーフ)
- 識別子をエスケープする(入力データバリデーションが妥当であることが前提。フェイルセーフ)
2、3の方法ではSQLクエリ生成の「出力の完全な無害化」を保証することは不可能です。しかし、一般的なアプリケーションであれば、入力データバリデーションが妥当なら「許容するリスク」として受け入れば可能なレベルのリスクです。
無害化レベルの強さは 1 > 2 > 3 になります。可能であれば強い無害化を選択する方が良いのは言うまでもありません。
- 厳格に利用を許可する識別子名をバリデーションする
$col_spec = [
VALIDATE_STRING, // 文字列としてバリデーション
VALIDATE_FLAG_NONE, // 配列による完全一致なのでフラグ必要無し
[
// そもそも長さがおかしい文字列データはエラー。バリデーションの最適化。
'min' => 4, 'max' => 10,
// 許可する文字列データの配列
'values' = [
'name' => true,
'first_name' => true,
'last_name' => true,
],
],
];
// バリデーションエラーで例外。エラー処理は例外に任せる。
$col = validate($ctx, $_GET['col'], $col_spec);
- 利用を許可する文字種だけバリデーションする
$col_spec = [
VALIDATE_STRING, // 文字列としてバリデーション
VALIDATE_STRING_ALNUM, // 半角英数字を許可
[
// そもそも長さがおかしい文字列データはエラー。バリデーションの最適化。
'min' => 4, 'max' => 10,
// 英数字に加え"_"を許可
'ascii' => '_',
// ” "(スペース)を許可するとリスクが急上昇するので許可しない!!
],
];
// 識別子名が無害であることは、コントローラー/モデルでバリデーション済みである前提。
// バリデーションエラーで例外。エラー処理は例外に任せる。
$col = validate($ctx, $_GET['col'], $col_spec);
- 識別子をエスケープする
// 識別子名が無害であることは、コントローラー/モデルでバリデーション済みである前提。
$col = pg_escape_identifier($col);
正規表現の無害化
正規表現にもエスケープシンタックスは定義されており、メタ文字はエスケープできます。ユーザー入力部分の正規表現を許可していない場合、正規表現メタ文字のエスケープ処理を”必ず”行い無害化します。
しかし、この事例の場合は”.”, “*”を許可するアプリ仕様なので、無条件の正規表現メタ文字のエスケープは行なえません。
開発者は識別子と同じく、裁量により許可するリスクを選べます。
- 正規表現文字列を厳格にバリデーションする
- 正規表現をエスケープする (ここの事例の場合は選べない。フェイルセーフ)
- 正規表現はバリデーション済みとする(フェイルセーフなし)
正規表現は1〜3のオプションで選択した後、SQLパラメーターとしてエスケープするかプレイスホルダを使ったプリペアードクエリによる無害化も行います。この為、3の「入力データバリデーションでバリデーション済み」として「正規表現として無害化」を行わなわない場合でも、「SQL文をインジェクションされるリスク」は発生しません。
3を選択した場合、ReDoSや正規表現インジェクションのリスクは無害化できませんが、入力バリデーションを行っていれば、一般的なアプリケーションであれば許容範囲内のリスクでしょう。
パラメーターの無害化
出力時のパラメーターの無害化も、開発者の裁量によりリスクを選べます。
- パラメーターを厳格にバリデーションする
- パラメーターをプリペアードクエリを使い、SQL文とパラメーターに分離する(フェイルセーフ、必須の2種類がある)
- パラメーターをエスケープする(フェイルセーフ、必須の2種類がある)
「出力先に対して無害」には「出力先でエラーにならない」も含まれます。範囲外の数値や長過る/短過ぎる/不正文字・形式を含む/壊れた文字エンコーディングの文字列などの出鱈目なデータを送ってエラーになってしまうSQLクエリは有害です。SQLエラーにならず、データベースに保存できてしまう場合はもっと有害です。
バリデーションについては既に紹介したので省略します。
プリペアードクエリによる無害化:
pg_query_params(
'SELECT id, ' . pg_escape_identifier('col') .' FROM tbl WHERE name ~ $1 LIMIT $2;',
[$_GET['pattern'], $_GET['limit']]);
エスケープによる無害化:
pg_query(
'SELECT id, ' . pg_escape_identifier($col) .' FROM tbl WHERE name ~ '.
pg_escape_literal($pattern). ' LIMIT '. pg_escape_literal($limit);');
まとめ
重要なので、もう一度。プリペアードクエリ/プレイスホルダだけでは不十分な対策にしかなりません。データバリデーションが欠かせません。エスケープも同じく、データバリデーションが欠かせません。
コンピュータープログラムは妥当なデータでないと正しく動作できません。出鱈目なデータをいくら処理しても正しい結果になりません。例外は”妥当な仕様”として出鱈目なデータをサニタイズ(無害化)する場合のみです。従って、データの妥当性検証(入力データバリデーション)が非常に重要なセキュリティ対策になります。
しかし、今あるWebアプリは入力データバリデーションが非常に貧弱か、全くないモノが大半です。
仕様としてサニタイズ(無害化)するのは基本的にはNGです。OWASP TOP 10:2017A10に脆弱なアプリとして分類するアプリになります。
アプリケーションとライブラリでは「セキュリティ対策の責任/設計が異なる」ことの理解も重要です。専用アプリケーションであるアプリセキュリティの責任は”アプリの責任”です。”ライブラリの責任”にする設計は、アプリ設計として脆弱か不適切な設計であるモノが大半です。
セキュアコーディングほど重要かつ基礎的な概念で、これほど多くの開発者に誤解されている事例はないです。”セキュリティ専門家”の責任は大きいと言えると思います。
セキュアコーディングの歴史は、防御的プログラミングから数えると、もうすぐ30年になろうとしています。回り道のしすぎ、ではないでしょうか!?
参考: