Webアプリの入力はバリデーションできない、という誤解

(Last Updated On: 2019年2月13日)

Webアプリの入力はバリデーションできない、と誤解している方は少なく無いようです。システム開発に関わる人でなければ誤解していても構わないのですが、システム開発者が誤解していると安全なシステムを作ることは難しいでしょう。

Webアプリの入力はバリデーションできない、と誤解している開発者にも理解できるよう噛み砕いて解説してみます。

入力とプログラムの基本構造

入力のバリデーション(入力の制御)はセキュリティ対策で全てのアプリケーションにとって最も重要なセキュリティ対策です。(SANS TOP 25 Monster Mitigation #1) 入力仕様を決めること、知ることはプログラマにとって重要な仕事の1つです。プログラムは特定の入力を受け付け、その処理を行い、何らかの出力を行います。プログラムが処理する入力を知らず(決めれず)にプログラムを作れません。 プログラムの基本構造は言語やアプリ、フレームワーク、OSを問わず同じです。入力があり、次に処理があり、最後に出力を行います。 プログラムの基本構造

   入力
     ↓
   処理
     ↓
   出力

関数やメソッドがプログラマにとって最も小さな単位の基本構造です。関数やメソッドの入力は引数(パラメータ)であり、出力は戻り値です。関数やメソッドにはプログラム内部の状態(変数)によって処理を行う場合もあり、入力と出力である引数や戻り値が必要ない場合もあります。この場合は状態が入力であり、出力であると言えます。

一般的にプログラムが受け付ける入力には何らかの「制限」や「規則」があります。これらは普通「仕様」と呼ばれます。入力仕様が決まっておらず、全く自由である事はまれです。 自分が作っているプログラムであれば、関数・メソッドの入力仕様は自然に決めていると思います。引数の1番目は整数、2番目は文字列、3番目は配列という形で決めていると思います。更にそれぞれの引数は整数の範囲は0〜100、文字列は英数のみで64文字まで、配列は特定のDBのレコード情報、などとして入力(引数)が妥当であるか確認している場合も多いでしょう。

関数引数のバリデーション

function foo($myint, $mystr) { 
  if ($myint < 0 || $myint > 100) { 
    // 無効な$myintの値
    throw; 
  } 
  if (strlen($mystr) > 64) { 
    // 無効な$mystrの値 
    throw; 
  } 
  ..... 
  return $some_useful_result; 
} 

入力のバリデーションとは?

入力のバリデーションとは”関数引数のバリデーション”の例のように、入力値が妥当であるか確認することです。言い換えると入力値が不正でないか確認します。「既知の良い物」(Known Good)を定義するのがセキュリティの基本です。「既知の悪い物」(Known Bad)を定義する「入力値が不正」であることチェックするとは考えずに「入力値が妥当」であるかと考えてチェックします。

バリデーションは入力のみに必要ではありません。呼び出される側(Callee)が入力バリデーションを行っていない場合、呼び出し側(Caller)が出力バリデーションを行う場合もあります。実際のプログラムでは以下のようなコードも多くあります。

呼び出し前の引数チェック

 if ($myint < 0 || $myint > 100) { 
  error('Invalid myint'); 
} else if (strlen($mystr) > 64) { 
  error('Invalid mystr'); 
}

// 関数を呼び出す。入力チェック済みなので安全。
$retval = foo($myint, $mystr); 

”呼び出し前の引数チェック”は関数呼び出しの前に出力先の入力仕様に合っているかチェックしています。チェックする代わりに不正な値を妥当な値に強制的に変換するプログラムもあるでしょう。

呼び出し前の引数変換

 if ($myint < 0) { 
  $myint = 0;
}
if ($myint > 100) { 
   $myint = 100;
}
if (strlen($mystr) > 64) {
   $mystr = substr($mystr, 0, 64);
} 

// 関数を呼び出す。入力を強制的に適合済みなので関数呼び出しは安全。
// 関数呼び出しは安全だが、セキュアコーディング的には異常値を無視するのはNG
$retval = foo($myint, $mystr);

堅牢なプログラムを作るためには入力と出力を確実にチェックする事が最も重要(SANS TOP 25 Monster Mitigation #1 & #2)ですが、全ての関数・メソッドで入力・出力バリデーションを行う事は以下の理由であまり現実的ではありません。

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

  • パフォーマンスが著しく低下する
  • プログラム開発の生産性が著しく低下する
  • メンテナンス性が著しく低下する

この為、実際のプログラム開発では全ての関数・メソッドで入力と出力チェックを行うのではなく、既に上流のプログラムでチェック済みの「信頼できる変数」としてチェックを省略します。

解説は省略しますが、1つだけ覚えておいて欲しい事があります。信頼境界線(Trust Boundary)を超えた変数はチェック済みであっても信頼できません。信頼できない変数は全て入力処理でチェックしなければなりません。

アプリケーションにおける入力バリデーションとは?

アプリケーションへの入力とは”外部”のシステムからの入力です。アプリケーションの入力バリデーションは、外部システムから”受け入れてはならない”入力があった場合、処理を拒否する為に確認(validation)を行います。ここでは解りやすく”受け入れてはならない”と書きましたが、入力バリデーションを行う場合には”受け入れて良い”条件を定義します。

アプリケーションへの入力が”外部”のシステムからの入力である、ということは信頼境界線の外からの入力ということです。つまり妥当でない入力は受け入れてはならないのです。基本的にリクエストに対してレスポンス返すだけのWebアプリでは入力を受け付けずHTTPレスポンス400のエラーを返します。Webアプリでは「レスポンスを返す=(リクエストに対しレスポンスする)プログラムを終了する」になります。

入力受け入れて再入力などを求める必要がある入力ミスと入力を受け入れず処理を拒否する入力バリデーションエラーは、根本的に異なる処理です。

入力バリデーションができない場合

実際のシステムではほとんどありませんが、入力バリデーションができない場合も在ります。入力フォーマットが全く定義されておらず、大きさも決まっていない場合にはバリデーションできません。しかし、このようなケースは極めて稀です。言い換えると、ほぼ全ての入力はバリデーションできます。

入力バリデーションエラーと入力ミス

入力バリデーションは上流(呼び出し側の関数・メソッド・アプリケーション)からの入力が「妥当」であるか判定し、それ以外の入力は拒否します。拒否する、とはプログラムを実行しない事を意味します。

入力ミスとはインタラクティブな入力などで「入力としては妥当」であるため受け付けるが、プログラムとしてはそのままでは処理を続行できない為、再入力が必要な入力です。インタラクティブな入力の場合、必ずしもプログラム実行を停止する必要はありません。これらの入力ミスは異常ではなく正常な入力です。正常系・準正常系の入力として処理します。

入力バリデーションエラーと入力ミスを区別する事は重要です。プログラムに入力を送ってくる入力元でより多くの制限を行っている場合は入力バリデーションエラーにできる入力が多くなり、入力ミスとして処理すべき項目が減ります。入力元での制限が少ないと、バリデーションエラーにできる入力が減り、入力ミスとして処理する入力が増えます。

バリデーションエラーと入力ミスでは処理方法が異なります。つまり、バリデーションエラーと入力ミスは異なる物として考えなければなりません。

アプリケーションはプログラムの組み合わせ

プログラムは「基本的なプログラムの組み合わせ」で作られます。つまり、より大きなプログラムは入力→処理→出力を組み合わせて作られます。

アプリケーションの基本構造

  [入力元]
     ↓
   入力 -+
     ↓     |
   処理 -+ 関数・メソッド
     ↓     |
   出力 -+
     ↓
   入力 -+
     ↓     |
   処理 -+ 関数・メソッド
     ↓     |
   出力 -+
     ↓
   入力 -+
     ↓     |
   処理 -+ 関数・メソッド
     ↓     |
   出力 -+
     ↓
   [出力先]

アプリケーションを組み合わせたシステムも基本構造は変わりません。

アプリケーションの基本構造

  [入力元]
     ↓
   入力 -+
     ↓     |
   処理 -+ アプリケーション
     ↓     |
   出力 -+
     ↓
   入力 -+
     ↓     |
   処理 -+ アプリケーション
     ↓     |
   出力 -+
     ↓
   入力 -+
     ↓     |
   処理 -+ アプリケーション
     ↓     |
   出力 -+
     ↓
   [出力先]

Webシステムの場合、次のような基本構造になります。

Webシステムの基本構造

  [入力元] Webクライアント
     ↓
   入力 -+
     ↓     |
   処理 -+ Webアプリケーション
     ↓     |
   出力 -+
     ↓
   [出力先] Webクライアント

安全なWebアプリケーションを構築する為に最も適切な入力バリデーションを行う場所は、Webアプリケーションの入力処理であることは明らかです。漏れ無くチェックするよう、全ての入力値を入力処理でチェックすると確実です。SANS/CWE TOP 25のMonster Mitigation(怪物的な緩和策 – 緩和策=セキュリティ対策)の#1に入力バリデーション(入力の制御)が挙げられているのは、入力値を確実にバリデーションすれば後の工程の処理・出力でのセキュリティ問題を大幅に軽減できる為に最も重要なセキュリティ対策のトップに挙げているのです。

Webクライアントからの入力バリデーション

前置きが済んだので本題のWebアプリで入力バリデーションが出来るのか?を解説します。

クライアント側プログラムで出力仕様を決めている場合

JavaScriptやFlashなど、クライアント側のプログラムで「特定の出力」(出力仕様)をなるよう処理している場合、Webアプリケーションでは「特定の出力」に合わせた入力仕様としてバリデーションを行い、それ以外はバリデーションエラーとして不正な値は受け付ける必要はありません

例えば、クライアント側でメールアドレスを以下の正規表現

^([a-zA-Z0-9])+([a-zA-Z0-9\._-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\._-]+)+$

にマッチすることを要求する仕様であるなら、Webアプリ側でも同じ正規表現にマッチする物のみを許可し、それ以外はバリデーションエラーにします。クライアント側で長さのチェックをしていない場合でも、サーバーであり得ないほど長いメールアドレス(512バイト以上のアドレスはほぼあり得ない)はバリデーションエラーとして処理して構いません。

ラジオボタン、ドロップダウンなどのバリデーションは解説の必要もないと思います。サーバーば送信した選択肢以外は全てバリデーションエラーです。

クライアント側プログラムで出力仕様を決めていない場合

HTMLフォームのみである場合もHTTP仕様やアプリケーション仕様で入力バリデーション可能です。HTTPの仕様ではクライアントはHTTPプロトコルで指定された文字エンコーディングでデータを送信する事になっています。GETメソッドを利用した場合の処理可能な最大長はそれほど多くなく確実に安全なURIは256バイト以下と言えます。
http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.2.1

普通のクライアントが送信可能なデータ量は2KB以下(IE、Safariの最大は約2KB。最近のバージョンはもう少し大きいかも)と仮定しても良いでしょう。HTTP仕様で決まっていなくても、フォームから送信されるデータはキーボードから入力可能な文字である(制御文字は含まれない)、とアプリケーション側で仮定しアプリケーション仕様としても構いません。

メールアドレスフィールドをバリデーションする場合、

  • 文字列が妥当な文字エンコーディングであるか?
  • 文字列はキーボードから入力として妥当な文字であるか?
  • メールアドレスとして妥当な長さであるか?

をバリデーションし、残りは入力ミスとして処理します。

ほぼ自由に入力できるテキストフィールド(textarea)をバリデーションする場合でも、

  • 文字列が妥当な文字エンコーディングであるか?
  • 文字列はキーボードから入力として妥当な文字であるか?
  • 妥当な長さであるか?

をチェックします。Webブラウザ側のプログラムで文字列の長さ制限をしていなくても、「1000文字まで」と長さ制限を記載しているにも関わらず、4000文字以上の文字列を送ってきた場合、バリデーションエラーとして処理を終了してしまっても構わないでしょう。このように妥当な制限を考慮し、設定するのも開発者の仕事です。どの程度までが妥当であるか?4000文字までなのか?4万文字なのか?はアプリの入力仕様として決める事です。

入力バリデーションエラーが発生した場合の処理

Webアプリケーションで入力バリデーションエラーが発生した場合、処理を停止しエラーページを表示して構いません。HTTPプロトコルではクライアント側に何らかの問題があった場合には400番台のHTTPステータスコードを返す事になっています。

  • 400:Bad Request – クライアントが不正なリクエストを送信
  • 401:Unauthorized – 未認証
  • 402:Payment Required – 支払いが必要
  • 403:Forbidden – アクセス禁止
  • 404:Not Found – リソース(ファイルなど)が無い
  • 405:Method Not Allowed – メソッド(POST/GET/PUT/DELETE/TRACEなど)が許可されていない
  • …以下省略 417まである

参考: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

HTTPプロトコル仕様によると400が最も適切なステータスコードです。バリデーションエラーの場合、HTTPステータスコード400を返すと良いでしょう。

入力バリデーションエラーが発生した場合、何らかの攻撃の可能性が高いのでリクエスト元の情報とエラーとなった入力値をログに記録しなければなりません。

まとめ

ほぼ全ての入力に対して入力バリデーションが行える事は明らかです。「入力バリデーションは行えない」と言うことはHTTPの仕様や自分が作っているクライアント側アプリの仕様、サーバー側アプリの仕様を知らない、と言っていることと同じです。開発者として不適格であると自分で言っているに等しいです。どうバリデーションできるか考える事をお薦めします。

  • 極一部の例外を除き、ほぼ全ての入力はバリデーション可能
  • アプリの場合、入力バリデーション仕様はデータ送信元アプリの出力仕様
  • 入力バリデーションエラーと入力ミスは区別する(入力ミスは正常系・準正常系)
  • 入力バリデーションは上流で処理する方が効果的(入力受け付け直後が良い)
  • バリデーションエラーの入力は処理しない(ログは必須)
  • バリデーションはホワイトリストで定義(ブラックリストは例外)

このエントリでは詳しく解説していませんが、セキュリティ的には入力バリデーションは何度行っても構いません。良いプログラミングは無駄(繰り返し)を排除するプログラミングですが、良いセキュリティ対策は無駄ばかりです。無駄ばかりというより、よりセキュリティ対策は敢えて”無駄”な処理や対策(繰り返し処理や対策)を行います。

例えば、文字エンコーディングのバリデーションは”アプリ”が入力バリデーションで完全にチェックしていれば、ライブラリやデータベースで何度も何度も繰り返し「無駄」にチェックする必要などありません。しかし、実際のシステムでは安全性を確保するために何度も「無駄」にチェックします。

プログラミングのベストプラクティスとセキュリティのベストプラクティスは相反する事を正しく理解していないと、おかしな事になります。これは別のエントリで解説します。

PHPのセキュリティ入門書に記載するコンテンツのレビューも兼ねてPHP Securityカテゴリでブログを書いています。コメント、感想は大歓迎です。

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

投稿者: yohgaki