大垣本を読んで「バリデーションはセキュリティ対策」について検討した

このエントリでは、セキュリティの観点から、バリデーション実装について検討します。大垣さんの本を読んで「大垣流バリデーション」について勉強した結果を報告します。

はじめに

大垣さんの記事「入力バリデーションはセキュリティ対策」では、「入力バリデーションはセキュリティ対策である」が力説されています。この記事はおそらくid:ajiyoshiさんのブログ記事「妥当性とは仕様の所作 - SQLインジェクション対策とバリデーション」を受けてのことだと思います。id:ajiyoshiさんのエントリでは、「妥当性検証は仕様の問題であってセキュリティ対策ではありません」と明言されています。私はid:ajiyoshiさんに近い考えを持っていますので、大垣さんの主張について、私なりに考えてみました。
記事を書くにあたり、徳丸の立場を明確にしておきたいと思います。

  • バリデーションの基準は仕様の問題
  • バリデーションは本来セキュリティ目的でするものではない
  • バリデーションにはセキュリティ上の効果はある
  • バリデーションについての徳丸の関心は、「基準は何で、何をするか」

以下、順に説明します。

バリデーションの基準は何か

バリデーションの問題を議論する上での最初の(そして最大の)問題は、「何を基準に(正として)バリデーションするか」です。セキュリティの教科書によっては、セキュリティの都合により入力値の制約を定め、その制約を元にバリデーションするように説明しているものがあります。その具体例については、コレコレを参照下さい。
id:ajiyoshiさんのブログでは、バリデーションの基準はアプリケーションの仕様だと明記されています。徳丸も同意で、私の本のP79には「入力値検証の基準はアプリケーション要件」という節があります。
大垣さんの本では、「入力値はポジティブセキュリティモデルを使用して確実に検証する(P171)」などとありますが、「ポジティブセキュリティモデルってなんだ?」と、よく分からないので、大垣さんとtwitterで会話してみました。togetterにまとめましたので参照下さい。結論としては、大垣さんも「バリデーションはアプリケーションの仕様(文書化されているかどうかは別として)に従う」と考えられているようです。
つまり、バリデーションの基準についてはid:ajiyoshiさんと大垣さんの考えは同じということになります。

バリデーションのセキュリティ上の効果

次に、バリデーションのセキュリティ上の効果について検討します。
id:ajiyoshiさんは、バリデーションのセキュリティ上の効果については言及されていないようです。これは実に潔い態度と言えますが、一般的には、バリデーションに一定のセキュリティ上の効果を認める人が多いと思います。このあたりの話題は、稿を改めて書きたいと思っています。
一方、大垣さんは、ご自身の本の中で以下のように書いておられます。

クロスサイトスクリプティングSQLインジェクション、HTTPレスポンススプリッテイングなど、入力さえ厳密に確認していればセキュリティホールとならずにすむ場合が数多くあります。

様々な脆弱性について防御上の効果があるが、常に攻撃を防げるわけではない、ということですね。これはセキュリティ観点から見たバリデーションの特徴です。
「さまざまな脅威に対して効果がある」という、いわば万能性に着目すると、「バリデーションはなんて素晴らしい『セキュリティ対策』なんだ」という評価になります。一方、常に防げるわけではない点に着目すると、「バリデーションで常に防御できるわけではないし、そもそも本質な対策ではない。そのようなものは『セキュリティ対策』とは認めない」という評価になるのでしょう。
つまり、バリデーションにどのような効果があるかについて見解が分かれているわけではなく、どの特徴に着目するかによって、バリデーションとは何かという表現が変わっていることです。「バリデーションはセキュリティ対策か否か」という問題の答えは「セキュリティ対策とは何か」という定義次第で変わるため、私はこの「セキュリティ対策か否か」という議論には興味がありません。

バリデーションの結果何をするのか

次に、バリデーションの結果「基準(=仕様)を満たさない」入力をどう扱うかについて考えます。
id:ajiyoshiさんのエントリには、『それ以外の入力については、「郵便番号を入力してください」というメッセージを表示して再入力を促す』などと例示されているので、入力画面に戻って再入力させるということですね。これは(曖昧な用語を敢えて使いますが)正常系の流れの一部とみなすことができ、「セキュリティ対策なんてもんじゃないよ」というid:ajiyoshiさんの主張もうなずけます。
一方、大垣さんの主張はどうでしょうか。先のエントリを読んでもよく分からなかったので、大垣さんの本から拾ってみました。同書のP168〜P169には、入力値の確認方法の「よい」例として以下のスクリプトが紹介されています。

if (is_number($_GET['int'])) {
  $n = $_GET['int'];
} else {
  trigger_error('数値ではありません。$n=', $n);
}
if ($min >= $n && $n >= $max) {
  trigger_error('数値が範囲外です。$n=', $n);
}

これを見ると、バリデーションの結果仕様外と判定された入力については、trigger_error関数を呼んでエラーとしています。trigger_error関数は、ユーザ定義のエラーを発生させる関数です。このスクリプトの後に、以下の説明が続きます。

trigger_errorで登録されたユーザーエラーハンドラで、無効なリクエストのエラー情報、送信したIP、ブラウザ、時間などを記録し、必要であればメールで通知したりアクセスを禁止しセッションを破棄するなどの処理を行えば、ほとんどのサイトでは十分でしょう。

かなり物々しいことが書いてありますね。このようなアプリケーションを利用することは、かなりの緊張感を伴いそうです。この前のところには、「人の年齢の場合は-10才などという数値はないはずですし、普通は0〜130くらいまでで十分でしょう」とあります。この文の内容には同意ですが、trigger_errorの処理内容とあわせて考えると、年齢を31と入れようとして手が滑って311と入力した場合でも、セッションが破棄され(結果としてログアウトし)、アクセスを遮断され、(管理者に?)メールで通知されるのでしょうか。
同書のAPPENDIX.Cには、P237からエラーハンドラの実装例が出ています。これを読むと、メール通信やアクセスの遮断はしていませんが、セッション破棄のコードはあります。

if (empty($_SESSION)) {
    $_SESSION = array();
}

empty関数は、変数が空の場合に真を返します。空とは、空文字列、0、NULL、FALSE、空の配列などを指します。このサンプルはプログラムの最初の方でsession_start()を呼んでいますので、$_SESSIONの値はなんらかの配列のはずです。すなわち、上のスクリプトは、$_SESSIONが空配列の場合に、あらためて空配列を代入していることになります。しかし、これは恐らくバグで、$_SESSIONが空でない場合に空にしたいのでしょう*1。以下、私の推定が正しいと仮定して議論を進めます。
エラーハンドラは、セッション破棄の他に、スタックトレース、$_GET、$_POST、$_SESSION、$_SERVERの全ての値をログに生成しています*2
そして、エラーハンドラの最後にexitでアプリケーションを明示的に終了しています。その前には、「//すべてのエラーは致命的なエラーとして処理」とコメントがついています。
これは中々タフなユーザー体験です。年齢や郵便番号などをうっかり間違えただけで、ログアウトしてエラー表示されるわけですから。複数のエラーがある場合、表示されるのは最初の1つのみです。このようなアプリケーションを使うと、かなり「残念な」思いをするように思います。一方、「なるほど、『セキュリティ対策』とはこれを指していたのか」という異様な説得力があります。
しかし、このような「セキュリティ対策」は本当に「世界の常識」なのでしょうか。Googleも、Facebookも、Amazonも「不正な入力」に対してそのような挙動は示さないようです。
現実にはそこまで厳しくしなくても、アプリケーションの安全性は保てると思います。バリデーションの結果、仕様外の入力があった場合は、間違いの理由を明示して再入力を促すのがよいと思います。「そんな対応ではセキュリティ対策とは言えない」ということであれば、バリデーションはセキュリティ対策でなくて結構と思いますし、それで安全性が損なわれるとも思えません。

まとめ

「入力バリデーションはセキュリティ対策」かという命題について検討しました。バリデーションの基準については、アプリケーションの仕様と言うことで一致しているようですが、仕様外の入力があった場合の挙動については、大垣本の教えは独特であることがわかりました*3
私自身は、バリデーションは「セキュリティ対策」の一種ととらえてもよい(どうでもいい)と思いますが、仕様外の入力に際しては利用者に分かりやすいメッセージを表示して、再入力を促すのがよいと考えます。

蛇足

先に引用した数値検証のスクリプトにはバグがあります。
※ このスクリプトは「間違い探し用のコードが手違いで収録されていました」と指摘されていますので、以下はその間違い探しの答えに当たりますね。

  • is_numberという関数はPHPには標準で用意されていない。is_numericという関数ならある*4
  • 「数値ではありません」の後の$nは初期化されていない変数を参照している
  • 範囲チェックのif文で等号があるのは間違い
  • 範囲チェックのif文は&&ではなく||にすべき

これらのバグの結果、実行時に未定義関数のエラーになりますが、is_numberをis_numericに置き換えて実行すると、$nの値に関わらず常にチェックを通ることになります。「数値が範囲外です」というエラーメッセージが表示されることはありません。

このスクリプトに続くのは「よい文字列確認」と題されたスクリプトです。以下に引用します。

if (strlen($_GET['name']) > 20) {
  trigger_error('名前が長すぎます');
} else if (strlen($_GET['name']) <= 1) {
  trigger_error('名前が短すぎます');
} else if (strlen($_GET['name']) != strspn('abcdefghijklmnopqrstuvwxyz')) {
  trigger_error('名前には小文字のアルファベットのみが使用できます');
}

ここにstrspnという関数が出てきます。以下の仕様です。

int strspn ( string $subject , string $mask [, int $start [, int $length ]] )
subject : 調べたい文字列。
mask : 許可する文字の一覧。
start : subject の中で調べ始める位置。
length : subject 内で調べる部分の長さ。

返り値 : subject の中で、全て mask の中の文字からなる最初のセグメントの長さを返します。

http://php.net/manual/ja/function.strspn.php

すなわち、第1引数の文字列の中から英小文字だけからなる部分を探しその長さを返すということなのですが、残念ながら第1引数('$_GET['name'])が抜けているので、実行時にエラーになります。これもバグですね。書きっぱなしで、テストはしなかったのでしょうね。
そもそも、strspnをバリデーションに用いるのはどうなのでしょうね。aからzを漏れなく入力しているか不安で仕方ありませんし、プログラムが読みにくく、なにか意図しない動作をしてしまいそうです。その懸念は、次のサンプルで現実のものとなります。
次のバリデーションサンプルは、入力文字列がSHA1ハッシュの要件(16進数40桁)を満たしていることを確認するものです。以下に主要部分のみ引用します。

// hex形式のsha1値は0-9, a-fまでの40文字
$error = (40 != strspn($str, '1234567890abcdef'));

40桁の16進数であることを確認したつもりなのでしょうが、このスクリプトだと、*先頭40文字が16進数* であることのチェックになります。要件を満たすには、文字列長が40であることも確認しなければなりません。
この結果、41文字目以降に攻撃文字列がある場合も、バリデーションをすり抜けます。以下に例を示します。

<?php
$str = "0123456789012345678901234567890123456789'or'a'='a";
$error = (40 != strspn($str, '1234567890abcdef'));
var_dump($error);
【実行結果】
bool(false)

$errorがfalse、すなわちエラーなしという結果になります。これはまずい。SQL呼び出し部分にSQLインジェクション脆弱性がある場合、任意のハッシュ値でチェック処理を通過できるなどの攻撃が考えられます。

脆弱性診断で上記が確認できた場合を考えましょう。SQLインジェクション脆弱性があればもちろん高危険度の脆弱性ですが、危険な脆弱性が発見されず、バリデーションの不備のみである場合、私なら低危険度、あるいは単なる指摘(Information)とします。
しかし、大垣さんのお考えですと、「入力バリデーションは非常に重要なセキュリティ対策の第一番目」ということですので、その「非常に重要なセキュリティ対策」に欠陥があるということは、上記は重大な脆弱性ということでしょうか。

この蛇足から導ける教訓は以下の通りです。

  • プログラムを書いたら必ずテストしよう
  • トリッキーな書き方は脆弱性の元なので平明な書き方にしよう

訂正(2011/12/26 8:36)

タイトルが「パリデーション」となっていたのでバリデーションに修正しました。本文も同様に2カ所修正しました。

[PR]
安全なWebアプリケーションの作り方DRMフリーのPDFによる電子版もあります。

*1:私が同じことをしたい場合は、if文なしに問答無用で空配列を代入してしまうと思います。

*2:もっとも、$_SESSIONはログデータを作る前に破棄しているので、意味がないように思えます。

*3:但し5年前に発行された本ですので、大垣さんの現在のお考えは変わっているかもしれません。

*4:is_numberをGoogle検索すると、「次の検索結果を表示しています: is_numeric」と表示されます