「ソフトウェアには入力バリデーションは必要ない」そんな事がある訳けないだろう?!いつも言っている事と真逆でしょ?!と思うでしょう。
しかし、「入力バリデーションが必要ないソフトウェア(=コード)」は沢山あります、条件付きですが。
TL;DR;
ソフトウェアの入力バリデーションが必要なくなる条件とは、ソフトウェアの信頼境界内で使う「部品」であることです。
- 部品/ライブラリなどなら入力バリデーションをせず、利用するアプリケーションに入力バリデーションをする責任を押し付けても(仕様にしても)OKです。
部品/ライブラリと異り、
- アプリケーションでは信頼できないデータソースから(信頼境界外から)の入力バリデーションは欠かせません。(必須です)
アプリケーションとライブラリでは役割/責任が異なります。役割/責任の違いによって、構造的に変えることの出来ない役割/責任、がアプリケーションソフトウェアには生まれます。基本的にアプリケーションは汎用の部品/ライブラリに入力バリデーションの責任を押し付けることはできません。
これは契約プログラミング(契約による設計)を理解すると、すっと腑に落ちると思います。
入力バリデーションが必要ないソフトウェア
メモリ管理バグは任意コード実行や秘密の情報の暴露など、致命的な問題の原因となります。正確なメモリ管理はセキュアなソフトウェアの基本です。
ですが、C/C++プログラマならよく知っているmalloc() – ヒープ領域にメモリを割り当てるAPI、は入力バリデーションが必要ないソフトウェアの代表例です。
include <stdlib.h>
void *malloc(size_t size);malloc() 関数は size バイトを割り当て、 割り当てられたメモリーに対する ポインターを返す。メモリーの内容は初期化されない。 size が 0 の場合、 malloc() は NULL または free() に後で渡しても問題の起こらない 一意なポインター値を返す。
https://linuxjm.osdn.jp/html/LDP_man-pages/man3/malloc.3.html
malloc()はsize_t sizeを引数として取り、指定されたsizeのメモリを割り当て、その先頭のアドレスのポインターを返します。メモリ不足などで割当れない場合はNULLポインターを返します。size_tは符号無し整数(通常符号無し64bit整数)なので、size引数をバリデーションする必要がありません。
C/C++でプログラムしたことが無い方でも「何を当たり前のことを?」と思ったかも知れません。しかし、現在ではかなり少なくなりましたが、malloc()で割り当てたメモリを正しく使えていないコードは数えきれないくらいあり、任意コード実行やDoS攻撃など、致命的なセキュリティ問題の原因になっています。
malloc()は与えられたsizeのメモリ領域を返すだけの役割(=責任)しか持っていません。おかしなsizeが与えられようが、割り当てたメモリへのアクセスでオーバーフロー/アンダーフローが発生しようがmalloc()の知った事ではないです。
正しいsizeのメモリを要求し、受け取ったメモリをオーバーフロー/アンダーフローすることなく利用する責任は呼び出したソフトウェア側にあるからです。
「ライブラリ」は正しく使うことを呼出側の責任にできる
malloc()は多少極端な例ですが、ライブラリ関数は正しく使うことを呼出側(≒アプリケーション)の責任にできます。
malloc()はメモリ割り当ての「汎用」APIで、どのようなsizeが妥当なsizeなのかは知ったことではないです。どのようなsizeであれば妥当なのか?は呼出側(≒アプリケーション)でなければ判りません。
malloc()と同様に大抵の汎用APIは、正しく使うことを呼出側の責任(≒入力値の妥当性検証の責任)とすることができます。
実例: 「ライブラリ」は正しく使うことを呼出側の責任にできる
「呼出側の責任」について解説するのに都合が良い、PHP 5.6のOnigurumaライブラリ(正規表現ライブラリ)のセキュリティ修正が2019年1月に行われました。
Mbstring:
Fixed bug #77370 (Buffer overflow on mb regex functions – fetch_token).
http://www.php.net/ChangeLog-5.php#5.6.40
Fixed bug #77371 (heap buffer overflow in mb regex functions – compile_string_node).
Fixed bug #77381 (heap buffer overflow in multibyte match_at).
Fixed bug #77382 (heap buffer overflow due to incorrect length in expand_case_fold_string).
Fixed bug #77385 (buffer overflow in fetch_token).
Fixed bug #77394 (Buffer overflow in multibyte case folding – unicode).
Fixed bug #77418 (Heap overflow in utf32be_mbc_to_code).
マルチバイト文字処理のMbstringモジュールの修正と記載されていますが、詳しくはバンドルしているOnigurumaライブラリの修正です。ざっくり言うと、不正な文字エンコーディングデータを渡された場合、不正なメモリアクセスが発生が発生しないようにされ、元のOnigurumaにはない「文字エンコーディングバリデーションコード」が追加されました。
OnigurumaはRubyでも利用されています。Rubyを利用されている方だと「そんな修正は知らないな」となるはずです。RubyとPHPでは異なるアプローチで入力値の妥当性検証の責任を処理しているからです。
※ Onigurumaは正しく使うことを呼出側の責任(≒入力値の妥当性検証の責任)としているはずです。その証拠に元のOnigurumaにはPHPと類似のパッチは当たっていません。
PHPの対応
PHPでは「不正な文字エンコーディングデータが送信された場合、ライブラリ側で対応すべき(=エラーにすべき)」という方針で修正されています。これはパッチを見れば明らかです。つまり、入力の妥当性検証(入力バリデーション)の責任はOniguruma側にあり、アプリケーションであるPHPにはありません。
この方針は間違いである、と思っています。
Rubyの対応
あまり詳しくソースコードを確認していのですが、ざっと見たところRubyではOnigurumaを利用する際にはOnigurumaが期待している文字エンコーディングであることを保証した文字列データを渡すようになっています。つまり、入力の妥当性検証(入力バリデーション)の責任はRuby側にあり、ライブラリであるOnigurumaにはありません。
この方針が正しい、と考えています。
PHPの対応が間違いでRubyの対応が正しい理由
PHPはセキュリティ修正でよくある「ライブラリ側に入力データバリデーションの責任を押し付ける間違い」をしています。
- フェイルファースト原則に従わない
- フェイルファーストなら不必要な性能劣化を生じさせている
- フェイルファーストなら発生しない出鱈目な結果となる場合がある
PHP 5.6.40のパッチを見れば、このパッチは正規表現マッチ性能を劣化させるパッチだと一目で解ります。ループや内部APIで文字の長さをチェックするパッチになっているからです。
しかもフェイルファーストではなく、Oniguruma内で問題が見つかった時、にエラーとしているので出鱈目な結果が返る場合もあります。おかしな入力データでも無理矢理におかしな結果を返す様では”信頼性”が高いプログラムは作れません。(信頼性はセキュリティの基本要素の一つ – ISO 27000)
Onigurumaが「妥当な文字エンコーディングデータ」を期待していることは、壊れた文字エンコーディングデータに対応するコードが無いことから明らかです。RubyではOnigurumaに渡される文字エンコーディングデータは「Onigurumaに渡される前の検証済み」(=フェイルファースト)であるようになっています。
フェイルファースト原則に従いアプリケーションであるRuby側で対応すれば、不要な性能劣化がない、出鱈目な結果を返さない、だけでなく
- UTF-16/32以外の文字エンコーディングの処理が「妥当な文字列データ」を期待している場合でも正しく動作することを保証できる
といったメリットもあります。
このPHPの脆弱性修正は壊れたUTF-16/32文字エンコーディングで不正メモリアクセスが発生するとするバグレポートにより行われました。Onigurumaは他の文字エンコーディングにも対応しています。「妥当な文字エンコーディングデータ」を入力として期待しているOnigurumaが「壊れた文字エンコーディングでもそれなりにメモリアクセスエラーを発生させない」ようになっているのか?は未知数です。コードを読んで/テストをして検証する必要があるように思えます。
結局のところ
- OnigurumaライブラリのアプリケーションであるRuby/PHPの責任として、文字エンコーディングの妥当性検証(入力バリデーション)を行う
これが最適解になります。
※ そもそもPHPがRubyと同様の実装、バリデーション済みの文字エンコーディングデータだけ、をOnigurumaに渡していれば「問題自体が存在しなった」ことになります。Rubyの設計/実装の方が遥かにセキュア(かつ効率的)だと言えます。
関連:
ライブラリ(ツール)とアプリケーションの責任
RubyもPHPも開発ツールです。Onigurumaライブラリから見るとRuby/PHPはアプリケーションですが、Ruby/PHPプログラムを書く開発者から見るとRuby/PHPはライブラリと同等の責任しか持っていません。つまり、
- 正しく使うことを呼出側の責任にできる
「責任にできる」というより「責任とせざるを得ない、仕組み的に」です。
Ruby(PHPもですが)は要所要所で文字エンコーディングのバリデーション(やサニタイズ)を行うようになっています。プログラミング言語はどこからどういったデータが入ってくるのか、それらのデータがどのようなデータであれば妥当なのか?全く判別できません。
※ 文字データはブラウザのリクエストや他のネットワーク、ファイル、データベース、メールなどありとあらゆる場所から送られてくる。
どこからどういったデータが入ってくるのか、それらのデータがどのようなデータであれば妥当なのか?これを定義する(できる)のはアプリケーションプログラムだけです。
このためツール(≒ライブラリ)であるプログラミングが要所要所で文字エンコーディングの妥当性検証をしていても、どこで不正文字エンコーディングによるエラー/例外が発生するか判りません。
プログラマが意図しない場所でのエラー/例外は、プログラマが意図しない結果を生みます。DoS状態になったり、最悪の場合は要所要所でチェックしているハズであっても不正文字エンコーディングが原因で不正メモリアクセスを許してしまう可能性もあります。
「アプリケーションとしてのRuby/PHPが文字エンコーディングの妥当性検証の責任を持つ」これと同様に「Ruby/PHPアプリケーションは文字エンコーディング妥当性検証の責任を持つ」としないと正しく動作すること保証することが極めて困難になります。
※ アプリケーションによる文字エンコーディングの妥当性検証(フェイルファーストによる検証)がないと、ライブラリなどが実行する文字エンコーディング妥当性検証により簡単にDoS状態になる事も多い。これは”アプリケーション”としてのRuby/PHPにも、”Ruby/PHPで作られたアプリケーション”にも当てはまります。
まとめ
ライブラリ(やツール)内で様々なデータバリデーションを行うことは重要です。欠かせない場合も多いです。
しかし、”ソフトウェアセキュリティ”に於てデータバリデーションの責任は「アプリケーションの責任」となります。
「ライブラリ」には入力バリデーションを必ず実施する責任はありません。
ツールである言語やライブラリであるフレームワークに全ての入力データバリデーションの責任を押し付けることは出来ません。ライブラリやツールは「汎用」でありできる限り多くのユースケースで使えるように作られます。「専用」であるアプリケーションにとって妥当なデータとはなにか?「汎用」であるライブラリやツールには判別できません。仕組的に不可能です。
Onigurumaの様に、アプリケーションから利用されることを前提とした「ライブラリ」などなら必ずしも入力バリデーションをする必要はありません。
「ソフトウェア」の絶対的な信頼境界となる「アプリケーション」では入力バリデーションは絶対に欠かせないです。アプリケーションでもプロセス/スレッドをまたがる場合(複数のプロセス/スレッドで一つのアプリケーションが構成される場合)、信頼境界の設定が必須になります、念の為。
※ 7PK(7つの悪質なソフトウェアセキュリティの領域)の「セキュリティ機能」では「セキュリティ機能はソフトウェアセキュリティではない」としています。ライブラリやツールのセキュリティ機能を使うと(誤った形で頼ること)、適切なソフトウェアセキュリティ対策にはならないです。今回紹介した事例(PHPプロジェクトによるOnigurumaセキュリティ修正)もそれにあたります。
※ 必ず妥当な文字エンコーディングであることが必要なら、Onigurumaで妥当性検証をすればよいのでは?と思うかも知れません。しかし、それは「基本的に無駄」です。アプリケーションでの文字エンコーディングの妥当性検証が必須だからです。多層防御はセキュリティの基本ですが、どこまで多層防御するのか?は基本的には「開発者の裁量」です。次のブログで紹介している「入力バリデーション」と「ロジックバリデーション」の何処かでバリデーションしていれば、残りはフェイルセーフ対策(≒多層防御)のバリデーションです。例えば、PCREはデフォルトでUTF-8文字エンコーディングのバリデーションを繰り返し行う実装になっています。これによりPCREの正規表現マッチが数百倍も遅くなるケースがあります。PCREはオプションで文字エンコーディングバリデーションを無効化できます。知る限りではHaskellのPCREバインディングはこのオプションをサポートしています。
参考: 契約プログラミングと信頼境界、CWE-20