一般に、オブジェクトは、例外条件が発生した場合であっても一貫した状態に維持されるべきである。特にセキュリティ上重要なオブジェクトにおいてはこれは必須である。オブジェクトの一貫性を維持する一般的な手法を以下に挙げる。
- 入力値検査 (たとえばメソッドの引数に対して)
- 例外条件を引き起こす可能性のあるコードがオブジェクトが変更される前に実行されるように、プログラムロジックを並び変える
- エラーの発生時にロールバックする
- 必要な処理をオブジェクトの一時コピーに対して行い、この処理が成功してはじめて変更を元のオブジェクトに反映する
- そもそもオブジェクトを変更しなくてもよいようにする
違反コード
以下の違反コードは、長方形の箱の3つの属性 length, width, height を持つ Dimensions クラスを示している。getVolumePackage メソッドは、梱包材の厚みを考慮して寸法の各辺に2を加算した上で、箱を保持するために必要な総容積を返すように設計されている。入力値の検証時に、箱の寸法として負の値(梱包材の厚みは除く)を排除している。寸法の最大値は10であり、引数として渡すことのできるオブジェクトのweightは20を超えてはならない。
weightが20を超える場合について考えてみよう。この場合、IllegalArgumentExceptionが発生し、独自のエラーレポーターによってインターセプトされる。この例外が発生しない場合、オブジェクトの元の状態がプログラムロジックによって復元されるが、例外が発生するとロールバックコードは実行されない。したがって、それに続く getVolumePackage() 呼び出しは間違った結果を返す。
class Dimensions { private int length; private int width; private int height; static public final int PADDING = 2; static public final int MAX_DIMENSION = 10; public Dimensions(int length, int width, int height) { this.length = length; this.width = width; this.height = height; } protected int getVolumePackage(int weight) { length += PADDING; width += PADDING; height += PADDING; try { if (length <= PADDING || width <= PADDING || height <= PADDING || length > MAX_DIMENSION + PADDING || width > MAX_DIMENSION + PADDING || height > MAX_DIMENSION + PADDING || weight <= 0 || weight > 20) { throw new IllegalArgumentException(); } int volume = length * width * height; // 12 * 12 * 12 = 1728 length -= PADDING; width -= PADDING; height -= PADDING; // 前の状態に戻す return volume; } catch (Throwable t) { MyExceptionReporter mer = new MyExceptionReporter(); mer.report(t); // 無害化 return -1; // 負のエラーコード } } public static void main(String[] args) { Dimensions d = new Dimensions(10, 10, 10); System.out.println(d.getVolumePackage(21)); // -1 (エラー) を出力 System.out.println(d.getVolumePackage(19)); // 1728 ではなく 2744 を出力 } }
このコードの catch 節は、「ERR08-J. NullPointerException およびその親クラスの例外をキャッチしない」の例外 ERR08-EX0 で認められている使い方であり、MyExceptionReporter クラスに例外を渡す一般的なフィルタの役割を果たす。 MyExceptionReporter クラスは、「ERR00-J. チェック例外を抑制あるいは無視しない」で推奨されているように、例外を安全に通知することに特化している。このコードはIllegalArgumentExceptionしかスローしないが、catch節は、tryブロックが他の例外をスローするように変更されても例外をキャッチできるように汎用的にコーディングされている。
適合コード (ロールバック)
以下の適合コードでは、getVolumePackageメソッド中のcatchブロックを、例外発生時にオブジェクトを前の状態に書き戻すコードに置き換えている。
// ... } catch (Throwable t) { MyExceptionReporter mer = new MyExceptionReporter(); mer.report(t); // 無害化 length -= PADDING; width -= PADDING; height -= PADDING; // 前の状態に戻す return -1; }
適合コード (finally 節)
以下の解決法では finally 節を使ってロールバックを行い、エラーの発生に関係なくロールバックが確実に行われるようにしている。
protected int getVolumePackage(int weight) { length += PADDING; width += PADDING; height += PADDING; try { if (length <= PADDING || width <= PADDING || height <= PADDING || length > MAX_DIMENSION + PADDING || width > MAX_DIMENSION + PADDING || height > MAX_DIMENSION + PADDING || weight <= 0 || weight > 20) { throw new IllegalArgumentException(); } int volume = length * width * height; // 12 * 12 * 12 = 1728 return volume; } catch (Throwable t) { MyExceptionReporter mer = new MyExceptionReporter(); mer.report(t); // 無害化 return -1; // 負のエラーコード } finally { // 前の状態に戻す length -= PADDING; width -= PADDING; height -= PADDING; } }
適合コード (入力値検査)
以下の適合コードは前述の適合コードを改良し、オブジェクトの状態を変更する前に入力値検査を行っている。try ブロックには例外をスローする可能性のある文のみを記述していることに注意。その他のコードはすべてtryブロックの外に移動している。
protected int getVolumePackage(int weight) { try { if (length <= 0 || width <= 0 || height <= 0 || length > MAX_DIMENSION || width > MAX_DIMENSION || height > MAX_DIMENSION || weight <= 0 || weight > 20) { throw new IllegalArgumentException(); // まず入力値を検証する } } catch (Throwable t) { MyExceptionReporter mer = new MyExceptionReporter(); mer.report(t); // 無害化 return -1; } length += PADDING; width += PADDING; height += PADDING; int volume = length * width * height; length -= PADDING; width -= PADDING; height -= PADDING; return volume; }
適合コード (オブジェクトを変更しない)
以下の適合コードでは、そもそもオブジェクトを変更しなくてよいようにしている。オブジェクトが不整合な状態に置かれることはなく、それゆえロールバックも必要ない。この手法はオブジェクトを変更する方法よりも好まれるが、複雑なコードでは使えないだろう。
protected int getVolumePackage(int weight) { try { if (length <= 0 || width <= 0 || height <= 0 || length > MAX_DIMENSION || width > MAX_DIMENSION || height > MAX_DIMENSION || weight <= 0 || weight > 20) { throw new IllegalArgumentException(); // まず入力値を検証する } } catch (Throwable t) { MyExceptionReporter mer = new MyExceptionReporter(); mer.report(t); // 無害化 return -1; } int volume = (length + PADDING) * (width + PADDING) * (height + PADDING); return volume; }
リスク評価
メソッドが処理に失敗した際にオブジェクトを前の状態に戻さないと、オブジェクトを不整合な状態にしてしまい、オブジェクトの不変条件に違反する可能性がある。
ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
ERR03-J | 低 | 中 | 高 | P2 | L3 |
関連する脆弱性
CVE-2008-0002には Apache Tomcat の複数のバージョンに発見された脆弱性について記載されている。引数の処理中に例外が発生すると、プログラムは間違ったリクエストのコンテキストに置かれたままになり、遠隔の攻撃者はこれを悪用してシステムのセンシティブな情報を取得することができた。Tomcat がこの処理を行っている最中にコネクションを切ることで、この例外を発生させることが可能であった。
関連ガイドライン
MITRE CWE | CWE-460. Improper cleanup on thrown exception |
参考文献
[Bloch 2008] | Item 64: Strive for failure atomicity |
翻訳元
これは以下のページを翻訳したものです。
ERR03-J. Restore prior object state on method failure (revision 62)