入力バリデーションで文字列の妥当性を検証(保証)しないと、不正文字問題の解決はできません。
よく「文字エンコーディングバリデーションは入力バリデーションしなければならない」と紹介はするのですが、その理由を詳しく解説していませんでした。これは文字エンコーディング攻撃の仕組みを理解してれば分かる事なのでしていませんでした。
しかし、文字エンコーディング攻撃の仕組みを理解していても必要なし、とする意見があるので理解り易く説明します。(理解りづらかったら教えてください)UTF-8のみですが、他の文字エンコーディングでも基本は同じです。
UTF-8文字エンコーディングの構造
UTF-8文字は1バイトから4バイトまでの可変長文字エンコーディングで以下の構造を持っています。
Number of bytes |
Bits for code point |
First code point |
Last code point |
Byte 1 | Byte 2 | Byte 3 | Byte 4 |
---|---|---|---|---|---|---|---|
1 | 7 | U+0000 | U+007F | 0xxxxxxx |
|||
2 | 11 | U+0080 | U+07FF | 110xxxxx |
10xxxxxx |
||
3 | 16 | U+0800 | U+FFFF | 1110xxxx |
10xxxxxx |
10xxxxxx |
|
4 | 21 | U+10000 | U+10FFFF | 11110xxx |
10xxxxxx |
10xxxxxx |
10xxxxxx |
出典:https://en.wikipedia.org/wiki/UTF-8
要するにByte 1のMSB(一番左のビット)から連続する1の数で、何バイトで1文字を構成するのか決る構造になっています。今回はコレだけ知っていればOKです。
UTF-8文字エンコーディング攻撃の手法
UTF-8文字エンコーディング攻撃は壊れたUTF-8文字エンコーディングデータで行います。それには2つの方法があります。
- 1文字を構成するハズのデータに不正なデータを混ぜる
- 1文字を構成するハズのデータを意図的に短くしたデータを混ぜる
話を簡単にする為、2文字になるUTF-8文字のケースを考えます。他の長さでも方法は同じです。
2文字になるUTF-8文字は以下の形式でなければなりません。
110xxxxx
+ 10xxxxxx
1文字を構成するハズのデータに不正なデータを混ぜる
UTF-8文字はその構造上、2バイト目にASCII文字コードが来ることはあり得ません。しかし、攻撃者が
abc + 110xxxxx
+ ”
このようなデータが来らどうなるでしょうか?
( + は見易くするためで文字列連結を意味します)
もし、処理系(ライブラリなど)が不正文字エンコーディングデータで処理を中止しない場合、3つのオプションがあります。
- 何もしない
- Byte 1だけ不正文字として除去
- Byte 1とByte 2を不正文字として除去
1だと問題を先送りするだけです。2だと ” が残ります。3だと ” が消えます。
‘SELECT col FROM my_table ORDER BY ‘. pg_escape_identifier($my_col1) . ‘, ‘.pg_escape_identifier($my_col2) .’; ‘
※ 識別子(カラム名)なのでプリペアードクエリは使えません!
のようなPostgreSQLのクエリ文があり、$my_col1、$my_col2 がユーザーが送った文字列だとします。pg_escape_identifier()は識別子エスケープをした後、前後に識別子ようのクオート文字である ” で囲みます。
識別子のエスケープは ” があった場合、”” と追加の ” を使ってエスケープします。
※ ここでは仮にpg_escape_identifier()が文字エンコーディングバリデーションをしない関数だとします。実際には不正文字エンコーディングの場合、FALSEを返します。
pg_escape_identifier($my_col1) の結果はそれぞれ、1、2、3のケースは以下のようになります。
- ” + abc +
110xxxxx
+ ” + “ - ” + abc + ” + “
- ” + abc + “
1と2は ” + “ とエスケープ後に “” が現れます。これはSQLインジェクション可能な構文破壊が可能なことを意味します。
※ 不正バイトを除去した後にエスケープすれば良いのでは?と思うかも知れませんが、ここではそれはあまり重要ではないので考慮しません。実装依存であることの紹介です。
3は大丈夫です。
1文字を構成するハズのデータに不正に短いデータを混ぜる
攻撃者が
abc + 110xxxxx
だけ送ってきた時にはどうなるでしょうか?
- 何もしない
- Byte 1だけ不正文字として除去
- Byte 1とByte 2を不正文字として除去
1、2、3のケースそれぞれ以下のようになります。
- ” + abc +
110xxxxx
+ “ - ” + abc + “
- ” + abc
1は 110xxxxx
+ ” と不正な文字データが現れます。これはSQL文を実行する処理系でどのように動作するかで問題になるか決まります。インジェクションが可能となる処理系もあるでしょう。※ 攻撃者にとって攻撃先は何でも良いことに注意。SQLはあくまでも例です。
2 は大丈夫です。
3 は ” + abc となります。これはSQLインジェクション可能な構文破壊が可能なことを意味します。
不正文字は何をしても問題になる可能性がある!
不正UTF-8文字があると何をしても問題になります。
- 何もしない
- Byte 1だけ不正文字として除去
- Byte 1とByte 2を不正文字として除去
1 は問題の先送りだけ。どこかで不正文字が問題になる可能性が残る。
2 は 「不正なデータを混ぜる」 で攻撃可能になります。
3は 「不正に短いデータを送る」で攻撃可能になります。
結局、不正な文字を渡してしまうと何をしても”処理系依存”で不具合の可能性があります。
※ 前述の通り、pg_escape_literal()は不正な文字エンコーディングでFALSEを返します。しかし、これで不正文字エンコーディング攻撃ができないので大丈夫、にはなりません。
一旦受け入れてしまった不正文字エンコーディングデータは問題の温床になり続ける
”処理系依存”の問題は根深いです。本来、不正な文字エンコーディングがある場合、処理を中止し失敗させなければなりません。しかし、少なくない開発者が「処理を中止するよりサニタイズ(悪い部分だけ取り除く)で何とかしよう」と考えてしまい、そう実装してしまいます。かと言って不正文字エンコーディング処理で「絶対にサニタイズせず、エラーにして失敗させる」が徹底されれば良いのか?というと良くありません。
バイナリセーフな処理系(そもそもバイナリを扱う機能)の場合、不正文字エンコーディングなど全く考慮しません。例えば、ファイルを保存する場合、中身など関係なく保存します。バイナリセーフな処理系は必要なので無くすことは不可能です。
つまり、”文字エンコーディングを識別する処理系”と”文字エンコーディングなど気にしない処理系”が同居するのが”普通”のシステムです。
不正文字を含むデータがバイナリセーフな処理系で保存され、後で文字エンコーディングを識別する処理系で使われると、そこで致命的エラーとなり処理が中止しDoS状態になり得ます。(保存/キャッシュされた壊れたデータを再利用しようとした時に致命的エラーとなると、何度でも致命的エラーが続きDoS状態になる)
このような構造的問題があるため、文字を処理する処理系の不正文字取り扱い動作を「致命的エラーとして処理を中止する」に統一しても問題は解決しません。
一旦、不正な文字エンコーディングをアプリケーション内に入れてしまうと構造的にインジェクションやDoS攻撃のリスクを排除できなくなります。
アプリケーション開発者はどうすべきか?
アプリケーション内に不正な文字を渡してしまうと何をしても”処理系依存”で不具合の可能性があります。開発者に3つのオプションがあります。
- 何もしない。攻撃されたら”処理系”(ライブラリ/外部システムなど)のせいにする。
- 全ての”処理系”が互いに問題なく動作する仕様であることを確認する。
- 出来る限り早い時点で入力バリデーションを行い正しい文字エンコーディングデータのリクエストだけ処理する(他は全て廃除)
1 は無責任です。しかも、バイナリセーフな処理を行う機能も在るため、仮に文字列処理を行う機能が完璧でも問題が発生します。
2 はC言語で実装されたライブラリなどまで調べるのは現実的には不可能です。バイナリセーフな処理を行う機能も在るため、仮に文字列処理を行う機能の動作を全て統一しても問題が発生します。
3 の入力バリデーションを出来る限り早い時点で行い、不正文字を含むデータは受け付けない。これが最も良い選択、というよりこれしか正しく動作することを保証できません。
参考:
まとめ
処理系によっては文字エンコーディングを自動的にチェックする場合もあります。
例えば、昔Railsで文字エンコーディング攻撃ができそうだ、という話になったとき「ビューで例外になるからOK」としたことがあります。これは
1. 何もしない。攻撃されたら”処理系”(ライブラリ/外部システムなど)のせいにする。
のアプローチです。
ビュー以外の”処理系”で問題があったらどうでしょうか?やはり処理系の悪いとするのだと思います。昨年、Onigurumaに不正文字データによるメモリ破壊バグがありましたが、もしこれが容易に任意コード実行が可能だった場合でも、処理系の問題だと済ましていたでしょう。
「攻撃されたら”処理系”(ライブラリ/外部システムなど)のせい」この対応が理想的!
とする人は少ないと思います。いくらライブラリや外部システムのせいにしても、DoS状態なってしまう問題はアプリケーション開発者の責任です。
「悪いモノを直せば/廃除すれば良い」とするブラックリスト型の対策は、いつもこのような問題を持っています。「正しいモノのみ受け入れる」ホワイトリスト型の対策必要です。
取り得る対策では
3 の入力バリデーションを出来る限り早い時点で行い、不正文字を含むデータは受け付けない。
これしか正しい選択はありません。壊れた文字エンコーディングデータを受け付けて処理しても、何も良い事がないのです。1
壊れた文字エンコーディング攻撃に限らず、問題を先送りすると余計な問題が発生し、対処が困難になることが多いです。任意の命令実行ができなくても、DoS状態になる可能性は低くありません。Fail Fast原則に則り、出来る限り早く失敗させるのが得策です。
参考:
セキュアコーディングはこういう状況になることも考慮して、「入力をバリデーションする」を第1原則にしています。
話しを簡単にするために文字エンコーディングの正規化にまつわる問題は省略しました。RFC 3454にも記載されていますが、文字エンコーディング規則を使うと同じ文字でも違う形2にエンコーディング可能です。処理系によってこのような不正な形のエンコーディングは致命的なエラーにする物もあれば、単純に正しい形に正規化してしまう物もあります。
この為、入力バリデーションをする場合は全ての必要な正規化が行われた後に検証する必要があります。例えば、NFDが必要な場合はNFDに正規化してから検証する、とされています。3
Webアプリの場合、ほぼ全ての入力が”テキスト”です。文字エンコーディングを入力バリデーションすると、大半の入力をバリデーションする事になります。どのみち大半の入力バリデーションするなら全ての入力の長さ、利用文字、形式でバリデーションするのが道理でしょう。
セキュアコーディングや契約プログラミングをすると、開発で楽できます!
参考:
- バリデーションですべきこと
- 構造化設計とセキュアコーディング設計の世界観は二者択一なのか? (構造化設計に捉われ過ぎが原因かも)
-
- 壊れた文字列データを修復するアプリ、といった特殊なアプリ以外は壊れた文字列を処理する必要はありません。何らかの理由で、どうしても壊れた文字列も受け入れなければならない場合だけ、入力バリデーションと同じく出来だけ早い段階で文字列をサニタイズ(悪い部分を除去)します。ただし、サニタイズはお薦めできる方法ではありません。現在は、悪い入力を受け付ける動作は「脆弱な動作」と考えられています。 ↩
-
- 例えばUTF-8では”0xC0 0xAB”がU+002B(+)と表記可能でした。現在はこういった異なる表記が可能場合、最短の表現形式のみが正しいエンコーディングとされています。 ↩
- NFC/NFD/NFKC/NFKDなどの正規化が必要な場合、その正規化コードが不正入力を検出し、エラーにするなら良いのですがしない物もあります。この為、正規化コードの仕様が判らない場合は生の入力がおかしくないかチェックしてから正規化し、その後にバリデーションする方が確実です。 ↩