チェック例外(checked exception)を抑制するために、空のあるいは何もしないcatch節で例外をキャッチすることはよくある。本来catch節は、プログラムが正しい不変条件(invariant)を満たす場合にのみ処理が継続するようにコーディングしなくてはならない。したがって、catch節では、以下のいずれかを行わなくてはならない。
- 例外処理から復帰する
- 例外を再びスローして次にその例外をキャッチするcatch節が復帰処理を行うようにする
- catch節のコンテキストで適切な別の例外をスローする
例外はプログラムの想定された制御フローを中断する。たとえば、tryブロックの中の、例外がスローされた時点よりも後に実行される式や文は評価されない。それゆえ例外は適切に処理しなくてはならない。例外を抑制しようとする理由の多くは、正当ではない。たとえば、クライアントプログラムが例外発生の原因となる問題から回復できないようなら、自身で例外をキャッチして抑制するよりも、プログラムの外部に伝播させるほうがよい。
違反コード
以下の違反コードでは、例外のスタックトレースを単に出力している。
try { //... } catch (IOException ioe) { ioe.printStackTrace(); }
例外のスタックトレースを出力するのは、デバッグが目的であれば役に立つかもしれないが、結局は例外を抑制してプログラムを実行するのと同じである。それどころか、実行中のプロセスの構成や状態に関する情報を攻撃者に漏らすことになりかねない。(詳細は「ERR01-J. センシティブな情報を例外によって外部に漏えいしない」を参照。)このコード例は、スタックトレースを出力することで例外に対処してはいるが、結局例外をスローされたかったことにして、プログラムの実行を継続する。つまりこれは、tryブロックの中で、例外が発生した部分のコードの後に実行されるはずの式や文は評価されないものの、プログラムの動作は例外の影響を受けないということを意味する。
適合コード (対話的)
以下の解決法では、ユーザに他のファイル名を入力するよう促すことでFileNotFoundExceptionに対処している。
volatile boolean validFlag = false; do { try { // 要求されたファイルが存在しない場合、FileNotFoundException をスローする // ファイルが存在する場合、validFlag を true に設定する validFlag = true; } catch (FileNotFoundException e) { // ユーザに他のファイル名を入力するよう促す } } while (validFlag != true); // ファイルを使用する
「ERR01-J. センシティブな情報を例外によって外部に漏えいしない」に適合するためには、ユーザは、ユーザディレクトリに存在するファイルのみに対するアクセスが許されるようにすべきである。こうすることで、ループの外に出るIOExceptionが、ファイルシステムのセンシティブな情報を漏えいすることを、防止できる。
適合コード (例外レポーター)
適切な例外条件の通知方法は、例外の発生する文脈に依存する。たとえば、GUIアプリケーションであれば、ダイアログボックスにエラーを表示するなど、グラフィカルな手段で例外を通知すべきであろう。ライブラリクラスの大半は、クラスのモジュール性を維持するためには、例外がどのように通知されるべきかを客観的に判断できなくてはならない。たとえば、System.errや特定のロガーに依存したり、特定のウィンドウ環境が利用できることを当てにすることはできない。そのため、例外を通知したいライブラリクラスでは、例外を通知するために使用するAPIを指定すべきである。以下の解決法では、例外を通知するためにライブラリが使用するインタフェースとして report()メソッドと、デフォルトの例外レポーターの両方を指定している。例外レポーターはサブクラスによってオーバーライドすることができる。
public interface Reporter { public void report(Throwable t); } public class ExceptionReporter { // 例外をコンソールに出力する例外レポーター(デフォルト) private static final Reporter PrintException = new Reporter() { public void report(Throwable t) { System.err.println(t.toString()); } }; // デフォルトの例外レポーターを保存 // ユーザはデフォルトの例外レポーターを変更できる private static Reporter Default = PrintException; // 将来デフォルトの例外レポーターをPrintExceptionに戻す際に便利 public static Reporter getPrintException() { return PrintException; } public static Reporter getExceptionReporter() { return Default; } // SecurityException 例外(未チェック例外)をスローするかもしれない public static void setExceptionReporter(Reporter reporter) { // 独自のパーミッション ExceptionReporterPermission perm = new ExceptionReporterPermission("exc.reporter"); SecurityManager sm = System.getSecurityManager(); if (sm != null) { // 呼び出し側に適切なパーミッションがあるかをチェック sm.checkPermission(perm); } // デフォルトの例外レポーターを変更 Default = reporter; } }
setExceptionReporter()メソッドは、センシティブな情報を漏えいしたり攻撃者のコンピュータなど不適切な場所にレポートを出力する冗長な例外レポーターを、悪意あるコードがインストールすることを防止している。これは、独自に設定したパーミッションExceptionReporterPermissionにexc.reporterが設定されている呼び出し元だけが例外レポーターを変更できるように制限することで、実現している。
ライブラリはcatch節の中でこの例外レポーターを使用する。
try { // ... } catch (IOException warning) { ExceptionReporter.getExceptionReporter().report(warning); // 例外から回復する }
必要とされる権限を持つクライアントコードは、エラーをログに保存したりダイアログボックスを提供するハンドラで、ExceptionReporterをオーバーライドできる。たとえば、Swingを使用するGUIクライアントプログラムでは、以下に示すように、ダイアログボックスを使用して例外が通知されなくてはならないだろう。
ExceptionReporter.setExceptionReporter(new ExceptionReporter() { public void report(Throwable exception) { JOptionPane.showMessageDialog(frame, exception.toString, exception.getClass().getName(), JOptionPane.ERROR_MESSAGE); }});
適合コード (例外レポーターをサブクラス化し、センシティブな情報を含む例外をフィルタリングする)
セキュリティ上の理由から、ユーザに例外を表示すべきでない場合もある(「ERR01-J. センシティブな情報を例外によって外部に漏えいしない」を参照)。そのような場合、デフォルトのreport()メソッドをオーバーライドするのに加えて、ExceptionReporterクラスをサブクラス化し、filter()メソッドを追加する方法を取ることもできる。
class MyExceptionReporter extends ExceptionReporter { private static final Logger logger = Logger.getLogger("com.organization.Log"); public static void report(Throwable t) { try { final Throwable filteredException = (t instanceof NonSensitiveException_1) ? t : filter(t); } finally { // ユーザへの必要な通知をすべて行う // (ダイアログボックスを表示する、コンソールに出力するなど) if (filteredException instanceof NonSensitiveCommonException) { logger.log(Level.FINEST, "Loggable exception occurred", t); } } } public static Exception filter(Throwable t) { if (t instanceof SensitiveForLoggingException_1) { // センシティブな情報はログに保存しない (ホワイトリスト) return SensitiveCommonException(); } // ... // ユーザに通知するためにリターンする return new NonSensitiveCommonException(); } }
report()メソッドはThrowableインスタンスを受け取る。したがって、すべてのエラー、チェック例外、未チェック例外を処理する。フィルタリングは「ホワイトリスト方式」に基づいて行われ、センシティブな情報を含まない例外のみがユーザに通知される。ログファイルに保存すべきでない例外についても、同様の方法でフィルタすることができる。(「FIO13-J. センシティブな情報を信頼境界の外に記録しない」を参照。)このアプローチを採用することで、例外チェーンの恩恵を受けることができる。各抽象化レベルに合った例外を通知できるだけでなく、将来故障解析を行う際に役立つ低レベルの原因をログに保存することもできる。
違反コード
スリープやウェイトの最中にスレッドが割り込まれた(interrupt)場合、java.lang.InterruptedException がスローされる。しかし、Runnableインタフェースのrun()メソッドはチェック例外をスローすることができず、InterruptedExceptionをキャッチして対処しなくてはならない。以下の違反コードでは、InterruptedExceptionをキャッチし、抑制している。
class Foo implements Runnable { public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { // 例外を無視 } } }
このコードでは、run()メソッドの呼び出し元は InterruptedExceptionが発生したかどうかを判断できない。それゆえ、Thread.start()などの呼び出しメソッドは例外に対処することができない。[Goetz 2006] 同様に、このコードが自身のスレッドで呼び出された場合、呼び出したスレッドは自身が割り込まれたかどうかわからない。
適合コード
以下の解決法では、InterruptedExceptionをキャッチし、interrupt()メソッドを現在実行中のスレッドで呼び出すことで割込まれた状態を保存している。
class Foo implements Runnable { public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 割込みされた状態をリセット } } }
こうすれば、呼び出し元メソッド(あるいはそのメソッドのなかのコード)は、割込みが発生したことがわかる。[Goetz 2006]
例外
ERR00-EX0: リソース解放の失敗がプログラムの動作に影響を与えない場合には、リソースの解放中に発生した例外を抑制してもよい。リソースの解放には、ファイルやネットワークソケットのクローズ、スレッドの停止などがある。多くの場合、これらのリソースは catch や finallyブロックで解放され、続くプログラムの実行中に再利用されることはない。したがって、リソースが枯渇しない限り、例外の発生はプログラムの動作に影響を与えない。リソースの枯渇を適切に処理することができる場合、将来プログラムを改善するときに備えて例外を無害化した上で保存すれば十分であり、それ以上エラー処理を追加する必要はない。
ERR00-EX1: ある抽象化レベルにおいて例外条件から回復することが不可能である場合、その抽象化レベルにあるコードでエラー条件を処理してはならない。そのような場合には、より上位レベルのコードで例外をキャッチし回復できるように、適切な例外をスローしなくてはならない。最も一般的な実装は、catchブロックを省略し、例外が通常通り外のブロックに通知されるようにすることである。
// 上位レベルにおいて例外から回復することが可能である場合 private void doSomething() throws FileNotFoundException { // 要求されたファイルが存在しない場合、FileNotFoundException をスロー // 上位レベルのコードで例外を処理。ダイアログボックスを表示し、 // ユーザに別のファイル名を入力するよう促す }
APIの中には、特定のメソッドがスローすることができる例外を制限するものもある。その場合、例外をキャッチした上で、それを許可された例外にラップしたり翻訳したりする必要があるかもしれない。
public void myMethod() throws MyProgramException { // ... try { // 要求されたファイルが存在しない // ユーザはファイル名を入力することができない } catch (FileNotFoundException e) { throw new MyProgramException(e); } // ... }
上位レベルのコードでも特定の例外から回復することができない場合、チェック例外を未チェック例外でラップしてスローするという方法もある。
ERR00-EX2: Threadクラスを拡張する場合、InterruptedExceptionをキャッチして抑制してもよい。[Goetz 2006] 割込み要求もまた、スレッドの割込みポリシーを実装するコードにより抑制してもよい。[Goetz 2006, p.143]
リスク評価
例外を無視したり抑制するとプログラムの状態に不整合が生じる恐れがある。
ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
ERR00-J | 低 | 中 | 中 | P4 | L3 |
自動検出
例外の抑制を検知するのは容易である。個々のケースがこのルールの違反や例外に該当するかどうかを判断するのは不可能である。ヒューリスティックなアプローチは有効であろう。
関連する脆弱性
AMQ-1272は ActiveMQ サービスの脆弱性について記述している。ActiveMQ サービスが Stomp クライアントから不正なユーザ名とパスワードを受け取った場合、セキュリティ例外が発生するがこれは無視され、結果として、接続されているクライアントプログラムはActiveMQに無制限にアクセスできる状態になった。
関連ガイドライン
MITRE CWE | CWE-390, Detection of error condition without action |
参考文献
[Bloch 2008] | Item 65. Don't ignore exceptions; Item 62. Document all exceptions thrown by each method |
[Goetz 2006] | 5.4, Blocking and interruptible methods |
[JLS 2005] | Chapter 11, Exceptions |
翻訳元
これは以下のページを翻訳したものです。
ERR00-J. Do not suppress or ignore checked exceptions (revision 98)