エラーと例外の使い方は異なります。エラーより例外が推奨されていますが、これはセキュアコーディング/セキュアプログラミングの考え方とも関連しています。この辺りを整理してみます。
エラー処理と例外処理の違い
「例外処理をエラー処理に使ってはならない」とよく聞くと思います。開発を始めた頃に「エラー処理の代わりに例外を使うべき、と言っているのに矛盾している!」と感じた開発者も少なくないと思います。これは矛盾しているのではなく、エラー処理と例外処理は役割が異なるので、例外処理をエラー処理に使ってはならないのです。
実用的なプログラム構築で大きな工数を必要とするのがエラー処理です。プロトタイプが簡単かつ短期間に構築できる大きな理由は「エラー処理を端折ることができる」からです。例外を利用すると”一部”のエラー処理を端折ることができ、プログラムが簡単になります。これは実際に実用的なアプリケーションを構築すると、ロジック処理では例外となる問題を考慮しなくても構わなくなるので、よく理解ると思います。
エラー処理と例外処理の違い
- エラー処理は”起きることが期待される問題”で、多くの場合、プログラムの実行停止は行えない
- 例外処理は”起きることが期待されない問題”で、多くの場合、プログラムの実行を停止しても構わない
これを理解していないとおかしな例外の使い方/エラー処理になります。
参考: 入力値を大別すると3種類しかない、と理解することがエラー処理と例外処理のどちらを利用すべきか?の判断の早道です。
エラーより例外が推奨される理由
発生する問題が少ければ、どのように問題を扱っても、問題を取り扱うコードは複雑になりません。しかし、プログラムを作る際には沢山の問題が発生する可能性を考慮する必要があります。プログラムは基本的に起こり得る全ての問題に対応する必要があります。
結果として”プログラムロジック”の中に”問題が起きた場合を想定するコード”を沢山記述しなけばならなくなり、エラー処理に為に本来はプログラム処理の中核であるハズのロジックが分りづらくなってしまう、といった状態になることがあります。
”エラー状態になった場合に求められる動作”(エラー時の仕様)になるようプログラムを作る必要がありますが、プログラムを作る目的は”エラー処理”ではありません。プログラムを作る目的は”特定の処理を実現する”です。”特定の処理を実現する”、この為にエラー処理が必要だ、とも言えるのですがエラー処理は主たる目的ではありません。
- 例外処理は”起きることが期待されない問題”
言い換えると”通常は起こらない/起きてはならない問題”です。これらの問題に対する処理を”プログラムロジック”の中に全て書くと”エラー処理だらけ”のコードになり、プログラム構築の目的の”ロジックのコード”が埋もれて見通しが悪くなります。
例外を使うべき状態
- ”通常は起こらない/起きてはならない問題”で、プログラムの実行を停止しても構わない問題(実行を停止せざるを得ない問題)
であるなら、問題が起きた時に勝手に安全にプログラム実行を停止させた方が良いです。”通常は起こらない/起きてはならない問題”が発生した場合は異常な状態であり、普通は、通常通りプログラムを実行できないからです。
- 絶対に必要なデータベース等の外部リソースへの接続ができない
- 絶対に正しく実行できない/想定外の入力データが送られてきた
- 絶対に必要な戻り値が戻ってこなかった
正しく接続できる、妥当なデータが送られてくる、妥当な戻り値がある、これらを”当たり前の前提条件”(普通はおかしくなることがあり得ない)とできる場合は例外を使うべきです。
例えば、CSPRNG(暗号学的に安全な乱数生成器)を使った乱数APIは
- システムが提供するCSPRNGにアクセスできる
- 必ず安全な乱数を返す
これらを絶対の前提条件にできます。乱数生成器にアクセスできなかったり、メモリ不足で乱数を返せなかった場合、プログラムが強制停止しても構わない問題として取り扱えます。これは”普通では起こりえない状態”だからです。
普通では起こりえないとは言っても、CSPRNGデバイスへのアクセス権がおかしく開けなかった、本来は即時に結果を返すはずのCSPRNGが何らかの問題で乱数を返さなかった、といった状態は絶対に起こらないとは言えない状態です。CSPRNGが生成する乱数はセキュリティ的に絶対に必要なデータです。取得できないならプログラムを停止せざるを得ません。
CSPRNGの問題はAPI内部で”例外”として処理した方が良い問題と言えます。例外を使えば、CSPARNG APIを利用するコードは単純にAPIを利用するだけ、万が一例外が発生した場合は例外処理でプログラム実行を安全に停止すれば良いだけです。
エラーを使うべき状態
- ”起きることが期待される問題”で、多くの場合、プログラムの実行停止を行えない問題
であるなら例外を使うべきではありません。代表例は”ユーザーの入力ミス”です。
システム構成の都合上、テキストインプットのWebフォームでユーザーの入力ミスをバリデーションしない/できない場合はおかしな入力を受け取っても
- 「無効な入力を検出しました。処理を停止します。この異常は管理者に報告されます。」
といったメッセージだけを表示してWebフォームの処理を終了させることはできません。普通は入力ミスを検出して「郵便番号には7桁の数字のみ入力できます。」といったエラーメッセージと共に再入力を促すWebフォームページをユーザーに返します。
Webフォームでも例外処理は行える
ドロップダウン/ラジオボタン/チェックボックスといったインプットタイプの入力は、通常は決まっています。攻撃者はWebフォームに書かれたデータを無視して好き勝手なデータを送れますが、そんな出鱈目なデータまで普通に処理する必要は全く無いですし、普通に処理してしまうと有害でしかありません。
ドロップダウン/ラジオボタン/チェックボックスといったインプットタイプの入力では例外が使えます。
テキストインプットであってもバリデーション可能です。Webクライアントは指定した文字エンコーディングで文字列データを送ってこなければなりません。攻撃者は出鱈目な文字エンコーディングデータを送ってこれますが普通に処理する必要は全く無いですし、普通に処理してしまうと有害でしかありません。攻撃者は大きすぎるデータも送ってきます。1KBの名前はどう見ても大き過ぎです。これを普通に処理してしまうと有害でしかありません。普通では入力できない制御文字(注:テキストボックスの場合、普通は改行さえ送れない)も送ってきます。不正な制御文字を含むデータを普通に処理しても有害でしかありません。
JavaScriptで形式をバリデーションしている場合、テキストインプットもサーバー側で厳格にバリデーションできます。
- 商品番号/ユーザーID
- 個数などの数値
- メールアドレス
- 郵便番号
といった形式が決まった入力データなら、不正な形式のデータは有害でしかないので例外が使えます。
<form>タグの中にあるべきデータ(フィールド)は分っているはずです。足りないフィールドがある場合だけでなく、余計なフィールドがある場合にも例外が使えます。
Webフォームで必要な例外処理には以下のようになります。
- 入力種別(パラメーター名)によって形式が決まっているのに形式外のデータ
- 出鱈目な文字エンコーディングデータ
- 大き過ぎる/小さすぎるデータ
- 不正な制御文字を含むデータ
これらは全て「本来はあってはならないデータ」であり、「有害でしかないデータ」です。例外処理で
- 「無効な入力を検出しました。処理を停止します。この異常は管理者に報告/記録されます。」
とだけメッセージを返しても構わない入力です。
Webフォームの入力データだけをバリデーションしても不十分
ほぼ全てのWebアプリは、不十分であっても、Webフォームの入力データバリデーションを行っていると思います。十分なWebフォームの入力データバリデーションを行っても、それだけでは不十分です。
- URLのクエリパラメーター
- HTTPのリクエストヘッダー(クッキー含む)
- Web APIからの戻り値(他のWebサービスから取得したデータ)
- 信頼できないソースからのデータを保存している内部リソースからのデータ
といったデータが存在します。
セキュアコーディングが求める入力バリデーション
セキュアコーディングでは厳格な入力バリデーションを求めています。
- CERT Top 10 Secure Coding Practices #1
- CWE/SANS Top 25 Monster Mitigations #1
- ISO 27000 Input Validation
全ての入力に対して、数値の範囲、文字データの使っている文字種/長さ/形式、パラメーターの過不足などをホワイトリスト型で検証しなければなりません。
Webフォームの入力、といっても<form>タグで定義した入力が外部入力となる訳ではありません。CWE/SANS Top 25 Monster Mitigations #1では
パラメーターまたは引数、クッキー、ネットワークから読み込み全てのデータ、環境変数、DNSの逆引き、クエリ結果、リクエストヘッダー、URLやその一部、Eメール、ファイル、データベース、アプリケーションにデータを提供するあらゆる外部システム。 これらの入力はAPI呼び出しによって間接的に取得されることがあることを忘れてはならない。
とHTTPのリクエストヘッダー/URLのパラメーター、APIなどを使って間接的に取得した入力も含め、あらゆる入力をバリデーションするよう求め
もし開発者が入力データが期待している物でないか確認していないなら、問題を起こしてくれ、と頼む事と同じである。
としています。
<form>に含まれている攻撃が送ってきた出鱈目/余計な入力データは勿論、URLのクエリ文字列/HTTPヘッダー(当然クッキーも)の出鱈目/余計な入力、その他すべての外部入力もバリデーションして異常な入力データを全て排除する必要があります。
参考: 既にエラー/例外の利用区別の基準として以下のブログは紹介済みですが、入力バリデーションで何をどうすべきか?を理解する為にも、入力の種類は3種類しかない、は役立ちます。
例外とセキュアコーディング
例外とセキュアコーディングの相性はとても良いです。
- 通常ではない状態を異常な状態として処理を停止させる「例外」はフェイルセーフ機能として優秀
- 正しく動作させる(≒通常/期待している状態であることを保証する)ことを目的とする「セキュアコーディング」は、異常な状態である「例外」の発生を最小限に止める
一般に、異常な状態の最大の発生源はプログラム外部からの入力データです。
できる限り早く異常な入力データをバリデーションすべし、とするセキュアコーディングは例外(フェイルセーフ)と一緒に使って初めて非の打ちどころの無い完璧な処理になる、と言ってよいくらいでしょう。
Javaプログラムの問題として有名なNull Pointer Exceptionがあるので、Javaプログラマならよく理解っていると思います。例外に頼ったプログラミングではマトモなプログラムは作れません。Null Pointer Exception回避にはNullが返って来た場合のチェックも重要ですが、如何にして「正常な状態」であることを保証するか?が重要であることを理解っていると思います。
まとめ
例外(エラーも)は闇雲に使えば良い物ではありません。効率的かつ問題を起こさない例外処理とするためには、論理的に妥当な例外の使い方をしなければなりません。例えば、Null Pointer Exceptionは異常な状態の検出にとても役立ちます。しかし、「Null Pointer ExceptionがあるからNull Pointer Exceptionで構わない」とならないのは明らかです。
参考: 壊れた文字エンコーディングをライブラリ任せにはできない
「正常な状態」であることを保証するには、「異常な状態」を発生させる原因(可能性)を出来る限り根本(=できる限り早く)から排除します。
例外を正しく使用すると
- 全ての入力データの妥当性を”入力”の時点で確認(異常なデータは異常な状態の最大の原因)
- 入力データの妥当性確認は出来る限り早く実施(異常なデータには何をしても異常な状態しか作れない)
- 妥当性確認漏れ、避けられない異常な状態のフェイルセーフ対策として例外を利用
といった使い方になります。
これは「セキュアなソフトウェアの構造」を作る事を意味します。
アプリケーション開発者とライブラリ開発者では異なる考え方になります。アプリケーションとライブラリでは”役割”が異なることが原因です。
アプリケーション開発者は全ての入力データが検証済みであることを保証する責任があります。専用ソフトウェアであるアプリケーションでないと「どのようなデータが妥当なデータなのか?」判らないからです。例えば、単純な数値バリデーションでも「クライアント側でバリデーションしているか?」により、アプリケーションサーバー側のバリデーション処理が大きく変わります。システムの仕様/構成を定義している”アプリケーション”でしか、適切にデータの妥当性を定義&検証できません。
ライブラリ開発者は呼び出し側は妥当なデータを送ってくることを期待(≒強制)し、例外でプログラム実行を停止させても構いません。契約プログラミングではこれを徹底して行います。一般にライブラリは汎用ソフトウェアであり、専用ソフトウェアに組み込んで使った場合の「正しい状態」、を仕様として定義不可能だからです。この為、ライブラリはできる限り汎用的に利用でき、どうしても正しく処理できないデータに対してのみ例外を利用します。
「異常なデータ」の検出と対応をライブラリ任せにすると、
もし開発者が入力データが期待している物でないか確認していないなら、問題を起こしてくれ、と頼む事と同じである。
CWE/SANS Top 25 Monster Mitigations M1
と同じです。問題を複雑化し問題を作ることになります。これについては既にブログで書いています。