JPCERT コーディネーションセンター

OBJ02-J. スーパークラスに変更を加える場合、サブクラスの依存性を保つ

コードをモジュール化し再利用性を高めるために、プログラムロジックを複数のクラスやファイルに分割することがよくある。スーパークラスに変更を加える場合(たとえばコードの保守時)には、サブクラスが依存しているプログラムの不変条件(invariant)を維持しなくてはならない。関係するすべての不変条件を維持しないと、セキュリティ上の脆弱性を引き起こすことがある。

違反コード

以下の違反コード例では、銀行取引関連の情報を保持するクラスAccountを使っている。セキュリティはこのクラスでは考慮されておらず、サブクラスであるBankAccountに任されている。クライアントアプリケーションは、セキュリティメカニズムを持つBankAccountを使用しなければならない。

private class Account {
  // 預金残高等すべての銀行取引データを保持
  private double balance = 100;

  boolean withdraw(double amount) {
    if ((balance - amount) >= 0) {
      balance -= amount;
      System.out.println("Withdrawal successful. The balance is : "
                         + balance);
      return true;
    }
    return false;
  }
}

public class BankAccount extends Account {
  // サブクラスで認証を行う
  @Override boolean withdraw(double amount) {
    if (!securityCheck()) {
      throw new IllegalAccessException();
    }
    return super.withdraw(amount);
  }

  private final boolean securityCheck() {
    // 口座の管理を行ってよいかを確認
  }
}

public class Client {
  public static void main(String[] args) {
    Account account = new BankAccount();
    // セキュリティマネージャによるチェックを実施
    boolean result = account.withdraw(200.0);
    System.out.println("Withdrawal successful? " + result);
  }
}

ある日、Accountクラスを保守するプログラマがoverdraft()というメソッドを新規追加した。しかし、BankAccountクラスを保守するプログラマは変更に気付かなかった。そのため、クライアントアプリケーションは悪意ある呼出しに対して脆弱になってしまった。たとえば、BankAccountオブジェクトに対してoverdraft()メソッドを直接呼び出すことができ、本来存在すべきセキュリティチェックを回避される。以下の違反コード例にこのような脆弱性を示す。

private class Account {
  // 預金残高等すべての銀行取引データを保持
  private double balance = 100;

  boolean overdraft() {
    balance += 300;     // 残高を超える引出しが行われた場合、300 を追加
    System.out.println("Added back-up amount. The balance is :"
                       + balance);
    return true;
  }

  // Account クラスのその他のメソッド
}

public class BankAccount extends Account {
  // サブクラスで認証を行う
  // 注: 前のバージョンから変更なし
  // 注: overdraft メソッドは上書きしない
}

public class Client {
  public static void main(String[] args) {
    Account account = new BankAccount();
    // セキュリティマネージャによるチェックを実施
    boolean result = account.withdraw(200.0);
    if (!result) {
      result = account.overdraft();
    }
    System.out.println("Withdrawal successful? " + result);
  }
}

このコードは期待通りに動作するが、危険な攻撃の余地を与えている。overdraft()メソッドではセキュリティチェックを行っていないため、悪意あるクライアントプログラムは認証なしでoverdraft()メソッドを呼び出すことができる。

public class MaliciousClient {
  public static void main(String[] args) {
    Account account = new BankAccount();
    // セキュリティチェックを行っていない
    boolean result = account.overdraft();
    System.out.println("Withdrawal successful? " + result);
  }
}
適合コード

以下の解決法では、BankAccountクラスにおいてoverdraft()メソッドをオーバーライドし、呼び出されるやいなや例外をスローするようにしている。こうすることで、overdraftの機能の誤用を防止している。この適合コードの他の部分には変更はない。

class BankAccount extends Account {
  // ...
  @Override boolean overdraft() { // オーバーライド
    throw new IllegalAccessException();
  }
}

設計上、親クラスの新規メソッドをオーバーライドせず直接サブクラスから呼び出せるようにしておきたいのであれば、新規メソッドの中でセキュリティマネージャのチェックを実施するという解決法もある。

違反コード (Calendar)

以下の違反コード例は、java.util.Calenderクラスのafter()メソッドとcompareTo()メソッドをオーバーライドしている。Calendar.after()メソッドの返り値は、Calendarが表す時間がObject引数が指定する時間よりも「後」かどうかを示すboolean値である。プログラマは、2つのオブジェクトが同じ日時を指す場合もafter()メソッドがtrueを返すようにこの機能を拡張したいと考えている。さらに、compareTo()をオーバーライドし、「曜日で比較」オプションを提供しようとしている(たとえば、ウィークデイかどうかを調べるために、今日の日付が週の最初の日かどうかを比較する。週のはじまりは国によって異なる)。

class CalendarSubclass extends Calendar {
  @Override public boolean after(Object when) {
    // Calendar.compareTo() を正しく呼び出す
    if (when instanceof Calendar &&
        super.compareTo((Calendar) when) == 0) {
      return true;
    }
    return super.after(when);
  }

  @Override public int compareTo(Calendar anotherCalendar) {
    return compareDays(this.getFirstDayOfWeek(),
                       anotherCalendar.getFirstDayOfWeek());
  }

  private int compareDays(int currentFirstDayOfWeek,
                          int anotherFirstDayOfWeek) {
    return (currentFirstDayOfWeek > anotherFirstDayOfWeek) ? 1
           : (currentFirstDayOfWeek == anotherFirstDayOfWeek) ? 0 : -1;
  }

  public static void main(String[] args) {
    CalendarSubclass cs1 = new CalendarSubclass();
    cs1.setTime(new Date());
    // 現在時刻より前の日曜日
    cs1.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
    // Wed Dec 31 19:00:00 EST 1969
    CalendarSubclass cs2 = new CalendarSubclass();
    // true を出力することが期待される
    System.out.println(cs1.after(cs2));
  }

  // Calendar の他の抽象メソッドの実装
}

java.util.CalendarクラスはcompareTo()メソッドとafter()メソッドを提供する。after()メソッドはJava SE 6 API仕様に以下のように記述されている[API 2006]。

after()メソッドは、この Calendar が、指定された Object の表す時刻より後の時刻を表すかどうかを返す。whenCalendarインスタンスである場合、このメソッドは
compareTo(when) > 0
と等価である。そうでない場合、このメソッドはfalseを返す。

このドキュメントには、after()compareTo()を呼び出すのか、あるいは compareTo()after()を呼び出すのかが明記されていない。Oracle JDK 1.6 におけるafter()の実装は以下のようになっている。

public boolean after(Object when) {
  return when instanceof Calendar
         && compareTo((Calendar) when) > 0;
}

この実装では、2つのオブジェクトはまず初めに、オーバーライドしたCalendarSubclass.after()メソッドを使って比較される。次にスーパークラスのCalendar.after()メソッドを呼び出し、残りの比較を行う。しかし、Calendar.after()メソッドは内部でcompareTo()メソッドを呼び出しており、これはCalendarSubclass.compareTo()を呼び出して処理を行う。つまり、CalendarSubclass.after()は実際にはCalendarSubclass.compareTo()を呼び出し、falseを返す。

サブクラスを使用するプログラマはCalendar.after()の実装の詳細を知らず、スーパークラスのafter()メソッドはオーバーライドしたサブクラスのメソッドではなくスーパークラスのメソッドだけを呼び出すと誤って想定している。「MET05-J. コンストラクタにおいてオーバーライド可能なメソッドを呼び出さない」は同様のプログラミングエラーについて解説している。

一般に、この手のエラーが発生する理由は、スーパークラスの実装に依存したコーディングをしていることにある。このような想定は最初は正しくても、スーパークラスの実装の詳細は何ら知らされることなく変更されることがある。

適合コード (Calendar)

「コンポジションと転送」(composition and forwarding、あるいは「デリゲート」)と呼ばれるデザインパターンを使用した解決法を示す[Lieberman 1986][Gamma 1995, p.20]。この適合コードでは、Calendar型の private なメンバフィールドを持つforwarderクラスを新規に導入する。これは継承ではなくコンポジション(composition)である。このメンバフィールドはCalendarImplementationつまりabstract Calendarクラスの具体的インスタンス実装を参照する。また、CompositeCalendarと呼ばれるラッパークラスを用意し、前述の違反コードのCalendarSubclassと同様のオーバーライドメソッドを提供している。

// CalendarImplementation オブジェクトは
// abstract Calendar クラスの具体的実装である
// Class ForwardingCalendar
public class ForwardingCalendar {
  private final CalendarImplementation c;

  public ForwardingCalendar(CalendarImplementation c) {
    this.c = c;
  }

  CalendarImplementation getCalendarImplementation() {
    return c;
  }

  public boolean after(Object when) {
    return c.after(when);
  }

  public int compareTo(Calendar anotherCalendar) {
    // CalendarImplementation.compareTo() が呼び出される
    return c.compareTo(anotherCalendar);
  }
}

class CompositeCalendar extends ForwardingCalendar {
  public CompositeCalendar(CalendarImplementation ci) {
    super(ci);
  }

  @Override public boolean after(Object when) {
    // オーバーライドしたバージョンを呼び出す
    // 例: CompositeClass.compareTo();
    if (when instanceof Calendar &&
        super.compareTo((Calendar)when) == 0) {
      // 週の最初の日である場合 true を返す
      return true;
    }
    // 週の最初の日との比較は行わない
    // 規定のエポックとの比較を使用
    return super.after(when);
  }

  @Override public int compareTo(Calendar anotherCalendar) {
    return compareDays(
             super.getCalendarImplementation().getFirstDayOfWeek(),
             anotherCalendar.getFirstDayOfWeek());
  }

  private int compareDays(int currentFirstDayOfWeek,
                          int anotherFirstDayOfWeek) {
    return (currentFirstDayOfWeek > anotherFirstDayOfWeek) ? 1
           : (currentFirstDayOfWeek == anotherFirstDayOfWeek) ? 0 : -1;
  }

  public static void main(String[] args) {
    CalendarImplementation ci1 = new CalendarImplementation();
    ci1.setTime(new Date());
    // 最後の日曜日 (今より前の)
    ci1.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);

    CalendarImplementation ci2 = new CalendarImplementation();
    CompositeCalendar c = new CompositeCalendar(ci1);
    // true を出力することが期待される
    System.out.println(c.after(ci2));
  }
}

ForwardingCalendarクラスの各メソッドは、内包するCalendarImplementationクラスに処理を渡し、返り値を得る。このメカニズムを転送(forwarding)と呼ぶ。ForwardingCalendarクラスの大部分は、CalendarImplementationクラスの実装から独立している。したがって、将来CalendarImplementationクラスが変更されることがあっても、ForwardingCalendarクラスを壊すことはなく、同様にCompositeCalendarクラスも影響を受けない。オーバーライドしたCompositeCalendarafter()メソッドを呼び出すことで、CalendarImplementation.compareTo()メソッドを呼び出し、必要な比較を行う。super.after(when)を使用するとForwardingCalendarに転送され、CalendarImplementation.after()メソッドが呼び出される。結果として、java.util.Calendar.after()CalendarImplementation.compareTo()メソッドを呼び出し、プログラムは正しくtrueを出力する。

リスク評価

サブクラスに及ぼす影響を考慮せずにスーパークラスに変更を加えると、脆弱性につながる恐れがある。スーパークラスの実装の変更を考慮せずに設計されたサブクラスは間違った動作をし、データの不整合や制御フローの管理ミスなどにつながる。

ルール 深刻度 可能性 修正コスト 優先度 レベル
OBJ02-J P4 L3
自動検出

適切に自動検出することは困難である。

関連する脆弱性

JDK 1.2においてjava.util.HashtableクラスのentrySet()メソッドが導入された結果、java.security.Providerクラスは脆弱になってしまった。Providerクラスはjava.util.Propertiesを拡張し、java.util.PropertiesHashtableを拡張している。Providerクラスは、暗号アルゴリズム名(たとえば"RSA")をその実装を提供するクラスにマップする。

Providerクラスはput()メソッドやremove()メソッドをHashtableから継承し、それらにセキュリティマネージャによるチェックを追加している。セキュリティマネージャによるチェックにより、悪意あるコードは新たなマッピングの追加や削除を行うことはできない。しかし、entrySet()の導入により、信頼できないコードがHashtableからマッピングを削除することが可能になってしまった。原因は、ProviderentrySet()をオーバーライドして必要なセキュリティマネージャのチェックを追加しなかったことにある[SCG 2009]。この問題は一般に fragile class hierarchy 問題と呼ばれている。

関連ガイドライン
Secure Coding Guidelines for the Java Programming Language, Version 3.0 Guideline 1-3. Understand how a superclass can affect subclass behavior
参考文献
[API 2006] Class Calendar
[Bloch 2008] Item 16. Favor composition over inheritance
[Gamma 1995] Design Patterns, Elements of Reusable Object-Oriented Software
[Lieberman 1986] Using prototypical objects to implement shared behavior in object-oriented systems
翻訳元

これは以下のページを翻訳したものです。

OBJ02-J. Preserve dependencies in subclasses when changing superclasses (revision 98)

Top へ

Topへ
最新情報(RSSメーリングリストTwitter