X

ほぼ全てのインジェクション攻撃を無効化/防止する入力バリデーション 〜 ただし出力対策も必須です 〜

(Last Updated On: )

入力バリデーションはセキュリティ対策として最も重要なセキュリティ対策です。なぜセキュリティ対策であるのか?を理解していない方も見かけますが「ほぼ全てのインジェクション攻撃を無効化/防止する入力バリデーション」の効果と拡張方法を見れば解るのではないでしょうか?

ソフトウェア開発者が知っておくべきセキュリティの定義/標準/ガイドで紹介しているセキュリティガイドラインでは入力バリデーションが最も重要なセキュリティ対策であるとしています。

厳格な入力バリデーションを行うと、開発者が意識しなくても、非常に多くの脆弱性を利用した攻撃を防止できます。今回は比較的緩い入力バリデーション関数でも、ほとんどのインジェクション攻撃を防止できることを紹介します。

重要:セキュア/防御的プログラミングでは入力と出力のセキュリティ対策は”独立”した対策です。どちらかをすれば良い、ではなく入力と出力の両方で対策を行います。よく誤解されるので注意してください。

重要 その2:ホワイトリストの基本中の基本は”デフォルトで全て拒否する”であることに注意してください。全て拒否した上で、許可するモノ、を指定しないとホワイトリストになりません。ホワイトリストによる入力バリデーションは”無害化”ではありません。”妥当性の検証”です。

重要 その3あまり広く理解されていませんが、セキュアコーディングの第1原則は「全ての入力をバリデーションする」、第7原則が「全ての出力を(エスケープ/API/バリデーションで)無害化するです。セキュアコーディングはISMS/PCI DSSなどの認証規格の要求事項です。

なぜこのようなブログを書いたか?というと入力バリデーションしていても、”ほぼ全てのインジェクション攻撃を無効化/防止できる入力バリデーション”でない場合、入力時のセキュリティ処理だけでは”残存リスク”としてインジェクション攻撃が可能になる、という事を理解して欲しいからです。

参考:

そもそもバリデーションとは?

バリデーションほど誤解されている有効なセキュリティ対策はないのではないでしょうか?バリデーションは様々な箇所で利用します。バリデーションはセキュアな構造に無くてはならない必須要素です。バリデーションとは何か?はこちらを参照してください。

単純かつ緩い制限であっても効果的な入力バリデーション関数

入力バリデーションは厳格に行うべきです。しかし、開発中に厳しい入力バリデーションを行うとアプリケーション仕様の変更に伴い、入力バリデーション仕様も変更しなければなりません。これでは開発速度が遅くなります。

今回は開発中に仕様変更があっても、あまり開発に影響せず、かつほぼ全てのインジェクション攻撃を無効化するバリデーション関数を作ります。

方針

  • 言語にはPHP 5.6以降を使う
  • 入力は全て文字列として扱う(Webアプリの入力は基本的に全てテキスト
  • 英数字を受け入れる
  • UTF-8エンコーディングを受け入れる
  • 文字列の長さは開発(開発中の動作確認)に影響しない程度の長さを許可する(60バイトまで)
  • バリデーション違反はエラーを発生させ、確実にプログラム実行を停止する

バリデーション関数

上記の方針に従ったバリデーション関数(validate_default関数)は以下の通りです。

<?php
/**
 * Basic input validation.
 *  - Input must be UTF-8.
 *  - PHP must be 5.6 and up.
 */
// Called upon any validation error.
function validation_exit($message, $val='') {
    http_response_code(403);
    trigger_error('Validation error: '.$message.'[' .rawurlencode($val).']', E_USER_ERROR);
    exit(1); // Make sure exit script.
}

// Default relatively safe validation function for all kinds of injections.
// Single line alpha numeric and UTF-8 is allowed.
// To allow more chars, use $regex_opt. (Obviously, it's a part of regex)
function validation_default($val, $min_len = 0, $max_len=60, $regex_opt='') {
    if (!is_string($val)) {
        validation_exit('Invalid type', gettype($val));     
    }
    if (strlen($val) < $min_len || strlen($val) > $max_len) {
        validation_exit('Too long value', strlen($val));
    }
    // Only UTF-8 is supported.
    // WARNING: This code assumes UTF-8 only script.
    if (ini_get('default_charset') != 'UTF-8') {
        validation_exit('Only UTF-8 is supported', $val);
    }
    if (!mb_check_encoding($val, 'UTF-8')) {
        validation_exit('Invalid encoding', $val);
    }
    // Allow only alpha numeric and UTF-8.
    // UTF-8 encoding:
    //   0xxxxxxx
    //   110yyyyx + 10xxxxxx
    //   1110yyyy + 10yxxxxx + 10xxxxxx
    //   11110yyy + 10yyxxxx + 10xxxxxx + 10xxxxxx
    // Since validity of UTF-8 encoding is checked, simply allow \x80-\xFF.
    if (!mb_ereg('\A[0-9A-Za-z\x80-\xFF'.$regex_opt.']*\z', $val)) {
        validation_exit('Invalid char', $val);
    }
}

// Test by GET value. Use "php -S 127.0.0.1:8080" or like, then
// access "http://127.0.0.1:8080/validate.php?v=TEST_VALUE_HERE".
error_reporting(-1);
ini_set('display_errors', 'On');
validation_default($_GET['v']);
echo 'Reached here: '.rawurlencode($_GET['v']);

バリデーション関数の動作確認

ソースコードの最後にテスト用のコードも記載されているので、簡単に動作確認できます。上記コードをvalidation.phpに保存した場合、validation.phpを保存しているディレクトリから

php -S 127.0.0.1:8080

と実行するとPHPのビルトインWebサーバーが起動します。Webブラウザから

http://127.0.0.1:8080/validation.php?v=1234

とアクセスすると、最後の行が実行され以下の出力が行われます。

Reached here: 1234

これ以降はアクセスしたURLを出力を一緒に記述します。

拒否する入力例

ヌル文字

http://127.0.0.1:8080/validation.php?v=%001234
Fatal error: Validation error: Invalid char[%001234] in /home/yohgaki/tmp/validation.php on line 11

改行

http://127.0.0.1:8080/validation.php?v=%0a1234
Fatal error: Validation error: Invalid char[%0A1234] in /home/yohgaki/tmp/validation.php on line 11

他の制御文字

http://127.0.0.1:8080/validation.php?v=%181234
Fatal error: Validation error: Invalid char[%181234] in /home/yohgaki/tmp/validation.php on line 11

“<“(他の記号全て同じ)

http://127.0.0.1:8080/validation.php?v=%3C1234
Fatal error: Validation error: Invalid char[%3C1234] in /home/yohgaki/tmp/validation.php on line 11

無効な文字エンコーディング

http://127.0.0.1:8080/validation.php?v=%E6%97%A5%E6%0C%AC%E8%AA%9E%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A
Fatal error: Validation error: Invalid encoding[%E6%97%A5%E6%0C%AC%E8%AA%9E%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A] in /home/yohgaki/tmp/validation.php on line 11

空白文字

http://127.0.0.1:8080/validation.php?v=abc%20def
Fatal error: Validation error: Invalid char[abc%20def] in /home/yohgaki/tmp/validation.php on line 11

許可する入力例

“abcde”

http://127.0.0.1:8080/validation.php?v=abcde
Reached here: abcde

“日本語あいうえお”

http://127.0.0.1:8080/validation.php?v=%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A
Reached here: %E6%97%A5%E6%9C%AC%E8%AA%9E%E3%81%82%E3%81%84%E3%81%86%E3%81%88%E3%81%8A

なぜこのバリデーション関数でほぼ全てのインジェクション攻撃が防げるのか?

可変長インターフェースを持つシステムに対するインジェクション攻撃の本質は

  • プログラムが出力先の入力仕様を理解せず、誤作動する入力を受け付けて出力してしまう

ことにあります。誤作動してしまう原因は以下のような物があります。

SQL

  • 文字列/識別子を区切る ‘ や “、LIKE文のメタ文字の %や_

HTML/JavaScript

  • タグの開始/終了文字の < , > 、属性の開始/終了文字の ” , ‘ 、エンティティ開始の&など

XPath

XQuery

OSコマンド

  • OSコマンドのエスケープ に代表的な危険文字を書いています。ただし、これだけではありません。OSコマンドを確実に安全にエスケープする良い方法はありません。

メールヘッダー

  • 改行インジェクション攻撃が可能。不正にメールを送信する攻撃に利用される。

書いていくときりがないので、これくらいにします。このバリデーション関数ではこれらの出力先が特殊な意味を持つ文字をほぼ全てを無効な文字として拒否しています。このため、インジェクション攻撃はできません。文字エンコーディングもバリデーションしているので壊れた文字エンコーディングを使った攻撃もできません。

バリデーション関数の評価

ここに記載していない文字でかなり危険な文字に” “(半角のスペース)があります。半角スペースもかなり危険です。例えば、SQL文の識別子をエスケープ無しで出力している場合、

$sql = "SELECT ".$_GET['field_name']." FROM mytable";

$_GET[‘field_name’]に “1; DROP TABLE mytable; –” が与えられた場合、

$sql = "SELECT 1; DROP TABLE mytable; -- FROM mytable";

というクエリになり、不正なSQL文を実行できてしまいます。このような攻撃を行うには” “(半角スペース)が欠かせません。(PostgreSQLなどはマルチステートメント実行が可能)

この例のバリデーション関数では” “(半角スペース)はもちろん、; や – も受け入れないのでバリデーション関数で防御できます。SQLリテラルを文字列で作る場合、あまり危険と考えられていない ; や – を許可しているとリスクが増える、と認識してしている方はあまり多くないのではないでしょうか?

SQL以外でも意味を持つことが多い記号文字を全て拒否しているので攻撃できません。例えば、HTML5で認められているクオート無しので属性値でエスケープを忘れていても、攻撃できません。

$mydiv = "<div width={$_GET['width']}> foo </div>";

のようなコードであっても、空白で区切ることができないのでクオート無しの属性でさえインジェクションできません。

またデフォルトだと最大長が60バイトに制限されているので、プログラムが利用しているライブラリなどにバッファーオーバーフロー脆弱性があったとしても、攻撃できないか攻撃できても困難なレベルにまで小くなっています。

参考:今時のShellcodeとセキュア/防御的プログラミング

つまり、この簡単かつ単純なバリデーション関数を使っているだけでも「ほとんど全てのインジェクション脆弱性を防止」できます

特殊文字とは

特殊文字とは”何らかの意味がある文字全て”です。最も一般的な文字は

  • ” ” (半角スペース)

です。これはプログラミング言語/コマンドではトークンの”区切り文字”になります。半角スペースは最も一般的といえる特殊文字ですが、忘れがちな特殊文字です。

  • 改行

も特殊文字です。HTTPやSMTPなどの”行に意味があるプロトコル”などでは重要な意味を持っています。

特殊文字は”コンテクスト”で決まります。HTMLならHTMLの特殊文字、SQLならSQLの特殊文字、JSONならJSONの特殊文字があります。

バリデーション関数の拡張

制御文字はもちろん、記号文字全てを禁止していますが開発中のダミーデータを入れる程度なら英数字/UTF-8文字を許可しているのであまり困る事はないと思います。しかし、改行も” “(半角スペース)も使えないようでは困る場合も多いと思います。

こういう場合はホワイトリスト型で許可すれば良いです。例えば、”\n”と” “を許可したい場合、

function validation_string($val, $min_len = 0, $max_len=60, $regex_opt='') {
    $regex_opt .= '\x0A\x20';
    validation_default($val, $min_len, $max_len, $regex_opt);
}

と”\n” (\x0A)と” “(\x20)を許可する文字として追加します。サンプルのテスト例を以下に書きます。

http://127.0.0.1:8080/validation.php?v=abc%20def%0a
Reached here: abc%20def%0A

このバリデーション関数の残存リスク

このバリデーション関数の残存リスクはエクササイズとして残しておきます。このバリデーション関数を使っていても攻撃できるケースを思いついた方はぜひコメントをお願いします。

※ ヒント:文字エンコーディング、外部システム、詐称、余計な物

一緒に”\n”と” “を追加したことによる残存リスクの増加も考えると良い演習になると思います。

ペネトレーションテスト(侵入テスト)をしている方なら、リスクが高い文字、一文字一文字を追加することによって増えるリスクを的確に答えることができると思います。しかし、一般の開発者はそこまで知る必要はありません。入力バリデーションでリスクを低減し、出力時に完全に安全な形で出力すれば良い※だけです。

※ これが結構難しかったりします。ここの本題ではないないので解説しませんが、難しい場合もあるので入力バリデーションで十分リスクを低減し、出力時に確実に安全になるよう心掛けることが重要です。

セキュリティ対策を正しく評価することは重要です。詳しくは実は標準の方が簡単で明解 – セキュリティ対策の評価方法をご覧ください。

参考:工夫すると制御文字無しの任意コード実行も可能です。ただし、制限(利用可能文字、文字数制限)が厳しければ厳しいほど攻撃が困難になることには変りありません。

入力バリデーションは第一のセキュリティ対策

ソフトウェア開発者が知っておくべきセキュリティの定義/標準/ガイドが最も重要なセキュリティ対策として紹介している理由は紹介したバリデーション関数の有効性で明らかだと思います。入力バリデーションはセキュリティ対策として最も大切です。

とは言っても多くの入力はここで紹介したようなバリデーション関数では使い物にならない、と思うかも知れません。使い物にならない入力もあります。しかし、多くの入力はここで紹介したバリデーション関数よりももっと厳しい条件でバリデーションできます。

  • データベースのレコードID: 数字のみ15桁まで(符号付き64整数の最大値より大きい数値。オーバーフローに注意)
  • 月:数字のみ2桁まで
  • 都道府県:予め決まった都道府県名のみ(1都1道2府43県)
  • 価格:数字のみ15桁まで(整数オーバーフローのリスクを考えると20億未満の方が良い)
  • ハッシュ値:数字+ABCDEF、必要な桁数(SHA256なら64文字)
  • 電話番号/郵便番号/さまざまなコード類/etc

プログラマなら予め仕様を熟知しているハズの入力値を厳格にバリデーションするだけで、多岐にわたるインジェクション攻撃のリスクをゼロにできます。万が一、うっかりがあった場合、ライブラリなどにバグがあった場合でも勝手に防御できてしまうボーナス付きのセキュリティ対策が入力バリデーションです。私がコード検査をして見つける致命的なセキュリティ問題でも、上記の種類のデータで「ついうっかり」してしまった例を結構見つけます。※

※ 厳格な入力バリデーションをしていても、出力で完全に安全にするべきですが、入力バリデーションをしていなくても「安全だ」と誤った仮定の基にそのまま出力しているコードは結構あります。 これはPHPプログラムに限ったことではなく、Java、.NET、Rails、JavaScriptなどのコード検査でも同じように見つかります。セキュアなコードでは入力/出力の両方で多層防御(実際には両方とも”境界防御”ですが)する必要があります。

もちろん、全ての入力をここで紹介したような入力バリデーション方法でバリデーションできません。もっと緩い条件のバリデーションが必要なケースも多くあります。サンプルのバリデーション関数より緩いバリデーション条件のバリデーションはリスクが在ることを認識しつつ、緩いバリデーションを使いましょう。

入力バリデーションと出力制御の関係

CERTトップ10セキュアコーディングプラクティスで分かり易く解説されています。

7. 他のシステムに送信するデータを無害化する

コマンドシェル、リレーショナルデータベースや商用製品コンポーネントなどの複雑なシステムへの渡すデータは全て無害化する。攻撃者はこれらのコンポーネントに対してSQL、コマンドやその他のインジェクション攻撃を用い、本来利用してない機能を実行できることがある。これらは入力バリデーションの問題であるとは限らない。これは複雑なシステム機能の呼び出しがどのコンテクストで呼び出されたか入力バリデーションでは判別できないからである。これらの複雑なシステムを呼び出す側は出力コンテクストを判別できるので、データの無害化はサブシステムを呼び出す前の処理が責任を持つ。

入力のセキュリティ対策である入力バリデーションと出力のセキュリティ対策であるエスケープ(エンコーディング)/セキュアなAPIの利用/バリデーションは独立したセキュリティ対策です。

オブジェクト指向設計の原則であるSOLIDのSRP(単一責任原則)をセキュアコーディングにもあてはめようとする方を時々見かけますが、これはセキュアなコードを記述する原則としては誤りです。プログラミング原則の1つは防御的プログラミングであり、防御的プログラミングでは境界防御(入力バリデーション)はもちろん縦深防御(多重のセキュリティ、出力対策としてのセキュリティ確保)を確実に行います。

参考:エンジニア必須の概念 – 契約による設計と信頼境界線

そもそも危険と思われる入力にはプログラマは注意を払っていると思います。もし今まで注意を払っていなかったとしても、確実に安全な方法でバリデーションできていない入力がある、出力時のセキュリティ対策は独立したセキュリティ対策として完全に安全化する必要がある、と理解するだけでも安全なプログラムを書くために大きな助けとなります。

まとめ

ここで紹介した内容を理解すれば、なぜセキュアプログラミングの第一の対策が入力バリデーションなのか理解できると思います。誤解が解けない方はコメント頂ければ幸いです。

論理的/科学的な入力バリデーションの必要性は形式的検証と組み合わせ爆発を知れば理解ります。

不必要(=有害)な入力を受け入れて正しく処理しよう、とすることは無茶であり、無理です。

入力対策の次に重要な対策は出力対策です。プログラムから何らか出力を行う場合、確実に全ての出力が安全に行わなければなりません。入力と出力のセキュリティ対策は独立したセキュリティ対策であることを忘れないようお願いします。

時々、不可解な入力仕様のアプリケーションを見ることがあると思います。例えば、パスワードにほとんど記号が使えない、などのアプリケーションを見た事がある方も居るのではないでしょうか?あまり原理主義的に厳しいバリデーションにしてしまうと、おかしなアプリケーションになってしまうので、適切に適用するようお願いします。

最後に、くれぐれもこの緩い入力バリデーション関数で運用に入らないようお願いします。入力仕様に合わせた適切/厳格な入力バリデーションを行うようにしてください。これは”インターフェース”が安定してきた開発の後の方で十分です。コードと異り、インターフェースは安定しています。オブジェクト指向でインターフェースを使うのもこのためです。セキュリティ対策としても安定したインターフェース部分でバリデーションすることは合理的です。

参考:

残存リスク

セキュリティ対策において残存リスクを分析するリスク分析が欠かせません。ある対策を実施しても必ずと言って良いほど残存リスクがあります。標準セキュリティのプロセスでは、”対策が根本的であるか?”とは考えずに”どのような残存リスクが残っているか?”を考えます。この方が安全だからです。

では、このバリデーション関数の残存リスクを考えてみます。このバリデーション関数は文字エンコーディングがバリデーションされ英数字とUnicode文字しか許可していません。ほとんどのインジェクション攻撃に対するリスクはこのバリデーションで十分なレベルで廃除できますが、以下のような残存リスクがあります。

  • 変数が整数値である場合、範囲がバリデーションされていないので、
    • 整数オーバーフローを起こす値が設定される
    • SQLのLIMIT句に利用された場合はDoSを起こす値が設定される
  • 変数の種別/特徴を考慮していないので、値が不正である可能性がある
    • 整数のはずが文字列、文字列のはずが整数など
    • 文字列の長さが短かすぎる、長すぎる
    • 入力値が集合(例:都道府県)の場合、不正が値を設定できる
    • 入力値が特定のフォーマット(例:郵便番号、電話番号)の場合、不正な値を設定できる
  • 個別の変数しかバリデーションしないため、
    • 余計な変数が設定される。余計な変数で困る実例として、古くはPHPのregister_globals=On、最近ではRailsのMass Assignment脆弱性がこれにあたる。最近見つかっているアプリケーションの脆弱性でも”余計な変数”が原因であるケースが複数ある
    • 必要な変数が設定されない。必要な変数がない場合も誤作動の原因になる。
  • PHPの文字エンコーディングチェック関数に脆弱性があるかも知れない。
    • mbstringの脆弱性は以前にも見つかっている
    • Unicodeの仕様の為、脆弱になる。例えば、古いUTF-8は6バイトまであり、同じ文字を複数形式で表現できた。あまり考えれないが将来のUnicode仕様の変更はあり得ないとは言い切れない。
  • バリデーションは文字列の正規化後に行わなければならないが、正規化は他の関数に任されている
    • 例えば、全角ー半角変換をバリデーション後に行うと脆弱になる
  • 正規表現を利用しているため、正規表現によってはReDoS攻撃を許してしまう
  • 入力対策なので当然ですが、処理または出力コンテクストによっては英数字だけでもインジェクションが可能
    • 例えば、TAGBEGINが”<“、TAGENDが”>”に変換されてしまうプログラムの場合、HTMLインジェクションに脆弱になる
    • 変数が”意味的”に妥当/安全であるかどうか?検証する責任はプログラムロジックにある
    • 出力の安全性は出力処理に責任がある
  • エラーメッセージにユーザーからの入力が含まれる為、エラーメッセージが攻撃に利用される可能性がある

まだまだありますが、このくらいにしておきます。

このバリデーション処理は多くのインジェクション脆弱性に対して”根本的”な解決策を提供します。しかし、上に記載したように多くの残存リスクを残しています。特定のリスクに対して”根本的”に対応した、とはいっても全てのリスクに対応するものではない、と理解する必要があります。

特定の対策が脆弱性に対して根本的であるかどうか?と考える意味はありません。意味がないどころか、”この脆弱性には根本的に対応したから大丈夫!!”と残ったリスクを全く考慮しないか、見逃すリスクが発生します。特定の対策が脆弱性に対して根本的であるかどうか?と考えることは有害であると認識すべきです。

例えば、「SQLインジェクション対策にはプレイスホルダを使えばOK」という考え方があります。プリペアードクエリは命令(SQL)とパラメーターを分離するので仕組み的に”パラメーター”だけは安全になります。

しかし、この対策には”インジェクションを完全に防げない”という致命的な問題があります。識別子やLIKEクエリのインジェクションさえ防止できません。SQLデータベースがサポートする正規表現、XML、JSON、場合によってコマンド実行など、SQLを経由して実行できるサブシステムへのインジェクションにも対応できません。これらの対応できていないリスクが残存リスクです。

残存リスクを考えないで「プレイスホルダを使っていればOK」と不十分な対策しか実施しない結果となった例は数えきれません。特定の脆弱性に対して根本的か?と考える思考方法自体にリスク1があります。

全てのセキュリティ対策(ISO27000でいうリスク対応)はゼロトラストで考え、残存リスクがないか?ある場合はどのように対応するか?を検討し、残ったリスクを管理しなければなりません。

参考:脆弱性/セキュリティホールを一つ一つ修正していくだけで十分に安全にできる、とする考え方ではシステムは守れないです。


  1. ホワイトリスト思考の方がブラックリスト思考より安全であることと同じ。特定の問題に対して完全であるか?と考えるとブラックリスト思考と同じような結果になる。完全/完璧な理由を考えるのではなく、漏れ無く対策しているかを考える。
yohgaki:
Related Post
Leave a Comment