ソフトウェアの脆弱性や攻撃に関するレポートの数は、驚くべき速さで増加の一途をたどっている。またそれらの多くは、セキュリティ上の注意を呼びかける技術文書として発行される結果となっている。企業や教育機関、政府、そして個人に対するこの脅威に対応するには、脆弱性のないソフトウェアが開発されなくてはならない。
ソフトウェアの脆弱性の多くはコーディングエラーが原因で作り込まれる。たとえば、2004年の National Vulnerability Database に登録されている2500の脆弱性の64%は、プログラミングエラーが原因で作り込まれた[Heffley 2004]。
Java は比較的セキュアなプログラミング言語である。明示的なポインタ操作は存在せず、配列や文字列の境界は自動的にチェックされる。ナルポインタ参照は捕捉され、型変換や整数演算の動作は言語仕様上定義されており、プラットフォームに依存しない。Javaに組み込まれたバイトコード検査は、これらのチェックが常に行われることを保証する。その上、Javaには包括的かつ粒度の細かなセキュリティ機構が備わっており、個々のファイルやソケットその他の、取扱いに注意すべきリソースに対するアクセスを制御することができる。
しかし、Javaプログラムの安全性が侵害されることはある。本章では、Javaで書かれたプログラムが攻撃されうる場合と、それらの攻撃に対処するためのルールについて紹介する。ルールのすべてが、Javaで書かれたあらゆるプログラムに当てはまるわけではない。ルールが適用されるかどうかは、多くの場合、ソフトウェアの運用方法、および、信頼に関する想定に依存する。
誤った信頼
多くの場合、プログラムは、サブシステムとして動作する複数のコンポーネントから構成され、各コンポーネントは1つ以上の信頼ドメインで動作する。たとえば、あるコンポーネントはファイルシステムにはアクセスできるがネットワークにはアクセスできず、別のコンポーネントはネットワークにはアクセスできるがファイルシステムにはアクセスできない、といった具合である。「相互信頼を前提としないシステム構成要素の分解(distrustful decomposition)」と「権限分離(privilege separation)」[Dougherty 2009]はセキュアデザインパターンの一例であるが、このパターンは、システムを互いに信頼し合わないコンポーネントから構成される設計にすることで、特権で動作するコードの量を少なくする。
ソフトウェアの各コンポーネントは、信頼境界(trust boundary)を越えてデータを送信することを許すポリシーに従うことはできても、信頼度を自ら決めることはできない。アプリケーションを配備する者は、システム全体のセキュリティポリシーを考慮した上で、信頼境界を定義しなくてはならない。セキュリティ監査の担当者は、その定義を用いることで、ソフトウェアが達成すべきセキュリティを適切にサポートしているかどうかを判断できる。
Javaプログラムは、内製のコードとサードパーティー製コードの両方を含むことがある。Javaは信頼できないコードの実行を許すように設計されており、サードパーティー製コードを専用の信頼ドメインで動作させることが可能である。そのようなサードパーティー製コードの public な API は信頼境界とみなすことができる。信頼境界をまたいでやり取りされるデータは、その正当性が保証されない限り、検証されるべきである。データの受信者やクライアントが検証を省略してもよいのは、データをそのまま使用する場合だけであり、それ以外の場合、外部から入ってきたデータは検証しなくてはならない。
インジェクション攻撃
コンポーネントの信頼境界の外部から取得したデータは細工されている可能性があり、インジェクション攻撃につながる可能性がある。図1-1に攻撃のシナリオを示す。
図 1-1. インジェクション攻撃
プログラムは信頼境界を越えて受け取ったデータが適切であり、細工されていないことを保証する段階を踏まなくてはならない。以下にそれらを示す。
検証: 検証(validation)とは、入力データがプログラムの期待する有効な範囲内に収まっていることを保証するプロセスを指す。このプロセスでは、クラスやサブシステムへの入力に対して課す不変条件(invariant)に入力データが合致しているとともに、プログラムが要求する型や数値の範囲に合致していることが求められる。
無害化: 多くの場合、データは、異なる信頼領域(trusted domain)に属するコンポーネントに対して直接渡される。データの無害化(sanitization)とは、入力データが、それを受け取るサブシステムの要件に合致していることを保証するプロセスを指す。また、信頼境界を越えてデータを出力する場合には、出力データがセキュリティに関する要件(情報漏えいやセンシティブなデータの公開に関する)に合致していることを保証することをも伴う。無害化のプロセスには、入力に含まれる望ましくない文字を、削除、置換、エンコード、エスケープするなどの操作が含まれる。無害化が行われるのは、入力を受け取った後の場合(入力値の無害化)もあれば、信頼境界を越えてデータを渡す場合(出力の無害化)もある。データの無害化と入力値検証は共存し、互いに補完し合う場合もある。データの無害化に関する詳細は、「IDS01-J. 文字列は検査するまえに標準化する」を参照。
正規化と標準化: 正規化(canonicalization)とは、データの損失を伴わずに入力を同等の単純な形式へ変換するプロセスを指す。標準化(normalization)とは、入力を、データの損失を伴いつつ同等の単純な(そして受け取り側が期待する)形式へ変換するプロセスを指す。攻撃者は検証ルーチンを悪用し、不正な文字列が取り除かれた結果が不正な(おそらくは細工された)文字列となるような攻撃を行うかもしれない。そのような攻撃を防ぐためにも、データを検証する前に正規化と標準化を行わなくてはならない。詳細は「IDS02-J. パス名は検証する前に正規化する」を参照。標準化は完全に組み立てた後のユーザ入力に対して行うこと。入力の一部を標準化したり、標準化した入力とそうでない入力をひとつに組み立ててはならない。
コマンドやプログラムに対する命令を指定する文字列を受け取るような複雑なサブシステムは特に注意しなくてはならない。このようなコンポーネントに渡される文字列データには、コマンドや命令を実行させる特殊な文字が含まれているかもしれず、ソフトウェアの脆弱性を引き起こす可能性がある。
コマンドや命令を解釈する可能性のあるコンポーネントの例を以下に示す。
- OSのコマンドインタープリタ (「IDS07-J. 信頼できない、無害化されていないデータを Runtime.exec() メソッドに渡さない」を参照)
- SQLと互換性のあるインタフェースを持つリポジトリ
- XML パーサ
- XPath 処理系
- LDAP ディレクトリサービス
- スクリプトエンジン
- 正規表現のコンパイラ
異なる信頼ドメインに属するコンポーネントにデータを渡さなくてはならない場合、送信者は、適切な符号化や無害化をデータに施すことで、送信するデータが、受信者の属する信頼ドメインにおいて適した形になっていることを保証しなくてはならない。たとえば、システムが細工されたコードやデータに侵入されたとしても、システムの出力が適切に符号化や無害化されているならば、多くの攻撃は失敗に終わるだろう。
センシティブなデータの漏えい
システムのセキュリティポリシーはどの情報がセンシティブであるかを定める。センシティブなデータには、社会保障番号、クレジットカード番号、パスワード、秘密鍵といった、ユーザに関する情報が含まれる。異なる信頼ドメインに属するコンポーネントがデータを共有する場合、そのデータは信頼ドメインを越えるとみなすことができる。Javaは、1つのプログラムの中で異なる信頼ドメインに属するコンポーネントが互いに通信することを許すため、信頼ドメインを越えてデータが送信される場合がある。異なる信頼ドメインで認証されたユーザがデータにアクセスする権限を持たない場合、データがそのドメインへ送信されないことを、システムは保証しなくてはならない。単純にデータを送信しないことで済む場合もあれば、信頼ドメインを越える可能性のあるデータからセンシティブなデータをフィルタリングしなければならない場合もあるだろう (図1-2)。
Javaのソフトウェアコンポーネントがセンシティブな情報を出力する場面は多数存在する。センシティブな情報の漏えい防止に関連したルールとしては以下が存在する。
- 「ERR01-J. センシティブな情報を例外によって外部に漏えいしない」
- 「FIO13-J. センシティブな情報を信頼境界の外に記録しない」
- 「IDS03-J. ユーザ入力を無害化せずにログに保存しない」
- 「MSC03-J. センシティブな情報をハードコードしない」
- 「SER03-J. 暗号化されていないセンシティブなデータをシリアライズしない」
- 「SER04-J. シリアライズと復元においてセキュリティマネージャによるチェックをバイパスさせない」
- 「SER06-J. 復元時には private 宣言された可変コンポーネントはディフェンシブコピーする」
図 1-2. センシティブなデータのフィルタリング
Javaにおけるインタフェース、クラス、クラスメンバ(フィールドやメソッド)はアクセス制御されている。アクセス制御は、アクセス修飾子(public, protected, private)、あるいはアクセス修飾子の不在(修飾子を指定しない場合のアクセス、パッケージプライベートアクセスとも呼ばれる)によって示される。
Javaの型安全性は、private、protectedあるいは指定なしの(パッケージプライベート)宣言が行われたフィールドがグローバルにアクセスされないことを意味する。しかし、Javaには数多くの脆弱性が存在し、たとえばリフレクションの誤用により、これらの保護が覆される可能性がある。この仕様は言語仕様にはっきり規定されており、Javaのエキスパートにとっては自明のことかもしれないが、それを知らない者にとっては落とし穴になる。たとえば、public宣言されたフィールドは、Javaプログラムのあらゆる所からアクセスされ、変更されうる(そのフィールドがfinal宣言されていない限り)。センシティブな情報をpublic宣言されたフィールドに保存してはならないことは明白である。プログラムを実行するJVMにアクセスできさえすれば、それらの情報にアクセスできるからである。
アクセス指定子 | クラス | パッケージ | サブクラス | ワールド |
---|---|---|---|---|
private | x | |||
指定無し | x | x | x* | |
protected | x | x | x** | |
public | x | x | x | x |
- 脚注*: 同一パッケージのサブクラスはアクセス指定子の指定がないメンバ(あるいはパッケージプライベート)にアクセスすることができるが、そのサブクラスは、パッケージプライベートであるメンバを含むクラスをロードしたのと同じクラスローダーによってロードされていなくてはならない。異なるパッケージに含まれるサブクラスはパッケージプライベートなメンバにはアクセスできない。
- 脚注**: protected メンバを参照するには、アクセスするコードが protected メンバを定義したクラスに含まれるか、メンバを定義するクラスのサブクラスに含まれていなければならない。サブクラスによるアクセスは、サブクラスが置かれているパッケージに関わらず許可される。
表1-1に単純化したアクセス制御ルールの一覧を示す。x はそのドメインからのアクセスが可能であることを表している。たとえば、「クラス」列の x は、クラス内に宣言されたメンバが同一クラスからアクセス可能であることを意味する。同様に、「パッケージ」列は、メンバが同一パッケージ内で定義されたクラス(あるいはサブクラス)からアクセス可能であることを意味する(ただし、参照するクラスとメンバの定義を持つクラスが同一のクラスローダーによってロードされていることが必要)。クラスローダーが同一でなくてはならないという条件は、パッケージプライベートメンバアクセスだけに適用される。
クラスおよびクラスのメンバには最小限のアクセス権を与え、攻撃コードがシステムのセキュリティを侵害する機会を最小限に抑えるべきである。センシティブなコードを含む(あるいは呼び出す)メソッドを外部に公開するインタフェースの使用は出来る限り避けるべきである。インタフェースはpublicアクセス可能なメソッドのみを規定しており、そのようなメソッドはそのクラスのAPIの一部となるからである(これは、APIにはインタフェースを使用すべしという、Bloch の推奨と正反対であることに注意[Bloch 2008, Item 16])。これに対する例外の1つは、可変オブジェクトのpublicアクセス可能な不変ビューを公開する、変更不可能なインタフェースを実装することである。(「OBJ04-J. 信頼できないコードにインスタンスを安全に渡すため、可変クラスにはコピー機能を実装する」を参照。) さらに、final宣言されていないクラスのビューがパッケージプライベートな場合であっても、そのクラスがpublicメソッドを持つ場合、誤用される可能性は残る。すべての入力を無害化し、必要なセキュリティチェックを完全に実施するメソッドであれば、インタフェースを通じて公開しても構わない。
入れ子クラスをprotected宣言することはあるかもしれないが、トップレベルのクラスにprotectedのアクセス範囲を使うのは不適切である。他のパッケージの悪意あるコードがサブクラスを作り、メンバーにアクセスするのを防ごうとして、finalでないpublicクラスのフィールドをprotected宣言してはならない。さらに、protected宣言されたメンバはそのクラスのAPIの一部であり、継続してサポートする必要がある。「OBJ01-J. データメンバはprivate宣言し、それにアクセスするためのラッパーメソッドを提供する」では、クラスのフィールドをprivate宣言することを要求している。
ウェブサービスの API のように、クラス、インタフェース、メソッド、フィールドなどが公開されている API の一部である場合、public宣言するかもしれない。その他のクラスやメンバはパッケージプライベートにするかprivate宣言すべきである。たとえば、セキュリティ上重要でないクラスについては、public static 宣言されたファクトリーメソッドを提供し、privateなコンストラクタを使ってインスタンスの管理を実装することが推奨される。
ケーパビリティの漏えい
ケーパビリティとは、伝達可能かつ偽造不可能な、権限を表すトークンである。ケーパビリティという言葉は Dennis と Van Horn によって導入された[Dennis 1966]。この言葉は、何らかのアクセス権の集合を持つオブジェクトを参照する値を意味する。ケーパビリティの仕組みに基づくOSで動作するユーザプログラムは、ケーパビリティを用いてオブジェクトにアクセスしなくてはならない。
Javaの各オブジェクトは偽造不可能なIDを持つ。Javaの==演算子は参照の同一性を評価するので、IDの同一性を調べるために用いることができる。オブジェクトのIDは偽造不可能であるため、操作が承認されていることを示す偽造不可能なトークンとして、オブジェクトへの参照を使うことができる[Mettler 2010a]。
権限(authority)の実体はオブジェクト参照であり、これがケーパビリティとして働く。権限は、実行中のコードが有する、副作用を伴わない計算を除くあらゆる作用に影響する。権限の影響範囲は、ファイルやネットワークソケットといった外部リソースに留まらず、プログラムのその他の部分と共有される可変のデータ構造にも及ぶ[Mettler 2010b]。
センシティブな操作を行うメソッドを持つオブジェクトへの参照は、そのオブジェクトを持つ者がそれらの操作を行うことを許可するケーパビリティ(あるいはオブジェクトにそれらの操作をリクエストすることを許可するケーパビリティ)として働く。したがって、そのようなオブジェクト参照自体をセンシティブなデータとして取り扱わなければならず、信頼できないコードに漏えいしてはならない。
ケーパビリティやデータが漏えいする意外な原因のひとつに、内部クラス(inner class)の存在がある。内部クラスは、それを内包するクラスのすべてのフィールドにアクセスすることができる。Javaのバイトコードはそもそも内部クラスをサポートしていないので、内部クラスは、OuterClass$InnerClassのように、一定の規則に従って生成された名称のクラスにコンパイルされる。内部クラスは、それを内包するクラスのprivateフィールドへアクセスできなくてはならないため、これらのフィールドに対するアクセス制御は、バイトコードではパッケージアクセスに変更される。つまり、バイトコードを手で編集すれば、private 宣言され通常はアクセスできないフィールドにもアクセスすることができるのである(具体例は"Security Aspects in Java Bytecode Engineering"を参照[Schonefeld 2002])。
ケーパビリティに関連するルールとしては、以下が存在する。
- 「ERR09-J. 信頼できないコードにJVMを終了させない」
- 「MET04-J. メソッドをオーバーライドあるいは隠蔽するときにアクセス範囲を広げない」
- 「OBJ08-J. 入れ子クラスから外側のクラスのprivateメンバを公開しない」
- 「SEC00-J. センシティブな情報を特権ブロックから信頼境界を越えて漏えいさせない」
- 「SEC04-J. センシティブな処理はセキュリティマネージャによるチェックで保護する」
- 「SER08-J. 特権を持ったコンテキストでは必要最小限の権限でオブジェクトを復元する」
サービス運用妨害
サービス運用妨害攻撃(Denial-of-service attack)は、ユーザがコンピュータのリソースを利用できない、あるいは十分には利用できないようにする。この問題は、通常、デスクトップアプリケーションよりも、永続的にサービスを提供するシステムにおいて大きな懸念となるが、いずれにせよ、すべてのアプリケーションで発生しうる。
リソースの枯渇によるサービス妨害
サービス運用妨害の問題は、リソースの使用が入力データと比べて不釣り合いなほど大きな場合に発生することがある。クライアントソフトウェアでは、リソースに関する問題にはユーザが対処することが想定されており、入力をチェックして過度のリソース消費が発生しないかどうかを調べることは必ずしも合理的ではない。しかし、そのようなクライアントソフトウェアにおいても、ファイルシステムを溢れさせるような持続型のサービス妨害につながる入力はチェックすべきである。
Secure Coding Guidelines for the Java Programming Language[SCG 2009]は、サービス運用妨害を引き起こすおそれのある攻撃として以下を挙げている。
- SVGやフォントファイルといったベクトル画像の画像サイズとして非常に大きな値をリクエストする。
- 「ZIP 爆弾」、ファイルサイズの小さなZIP、GIF、gzipでエンコードされたHTMLコンテンツが、極端に大きな圧縮率のために、圧縮解凍時に膨大なリソースを消費する。
- "Billion laughs attack"、 XMLエンティティの展開により、XMLドキュメントが膨大なサイズに膨れ上がる。この攻撃は、XMLConstants.FEATURE_SECURE_PROCESSING の設定で上限を設けることで防ぐことができる。
- 過度のディスク容量の使用。
- 同じハッシュコードを持つ多数のキーをハッシュテーブルに挿入し、アルゴリズムの性能が平均のケース(O(n))ではなく、最悪のケース(o(n^2))になるようにする。
- サーバが各接続に対して著しい量のリソースを割り当てるような接続を多数行う(たとえば古典的な SYN flood 攻撃など)
サービス運用妨害攻撃とその防止に関するルールには以下がある。
- 「FIO03-J. 一時ファイルはプログラムの終了前に削除する」
- 「FIO04-J. 不要になったリソースは解放する」
- 「FIO07-J. 外部プロセスに IO バッファをブロックさせない」
- 「FIO14-J. プログラムの終了時には適切なクリーンアップを行う」
- 「IDS04-J. ZipInputStream からファイルを安全に展開する」
- 「MET12-J. ファイナライザは使わない」
- 「MSC04-J. メモリリークしない」
- 「MSC05-J. ヒープメモリを使い果たさない」
- 「SER10-J. シリアライズの過程でメモリリークやリソースリークをしない」
- 「TPS00-J. スレッドプールを使用しトラフィックの大量発生による急激なサービス低下を防ぐ」
- 「TPS01-J. スレッド数に上限のあるスレッドプールで相互に依存するタスクを実行しない」
- 「VNA03-J. アトミックなメソッドをまとめた呼び出しがアトミックであると仮定しない」
並行性に関するサービス運用妨害
サービス運用妨害攻撃のなかには、スレッドのデッドロック、スレッド枯渇、競合状態といった問題を引き起こすものもある。
プログラムの並行性に関する問題が原因となって発生するサービス運用妨害の防止に関するルールとしては、以下がある。
- 「LCK00-J. 信頼できないコードから使用されるクラスを同期するにはprivate finalロックオブジェクトを使用する」
- 「LCK01-J. 再利用されるオブジェクトを使って同期しない」
- 「LCK07-J. デッドロックを回避するためにロックは同一順序で要求および解放する」
- 「LCK08-J. 例外発生時には保持しているロックを解放する」
- 「LCK09-J. 途中で待機状態になる可能性のある操作をロックを保持したまま実行しない」
- 「LCK11-J. 一貫したロック方式を定めていないクラスを使用する場合、クライアントサイドロックを行わない」
- 「THI04-J. ブロックしているスレッドやタスクが確実に終了できるようにする」
- 「TPS02-J. スレッドプールにサブミットするタスクは割込み可能にする」
- 「TSM02-J. クラスの初期化中にバックグラウンドスレッドを使用しない」
その他のサービス運用妨害攻撃
サービス運用妨害攻撃の防止に関するその他のルールには以下がある。
- 「ERR09-J. 信頼できないコードにJVMを終了させない」
- 「IDS00-J. SQL インジェクションを防ぐ」
- 「IDS06-J. ユーザからの入力を使って書式を組み立てない」
- 「IDS08-J. 信頼できないデータは regex に渡す前に無害化する」
サービス運用妨害につながりうる問題
それ自身がサービス運用妨害を引き起こすことはないが、その発生につながりうる脆弱性に関するルールを以下に挙げる。
- 「ERR01-J. センシティブな情報を例外によって外部に漏えいしない」
- 「ERR02-J. ログ保存中の例外発生を防ぐ」
- 「EXP01-J. null ポインタ参照しない」
- 「FIO00-J. 共有ディレクトリにあるファイルを操作しない」
- 「NUM02-J. 除算と剰余演算でゼロ除算エラーを起こさない」
シリアライズ
オブジェクトをシリアライズ(serialization)することで、オブジェクトの状態をバイトストリームに書き出すことができる。[Sun 2004b] この仕組みを利用することで、オブジェクトの状態を保存し、将来再度オブジェクトとして復元(deserialization)することができる。また、シリアライズの仕組みは、遠隔メソッド呼出し(Remote Method Invocation, RMI)によりJavaのメソッド呼び出しをネットワークを通じて送信することを可能にしている。オブジェクトはシリアライズされ、分散した仮想マシン間で交換され、そして復元される。シリアライズの仕組みはJavaBeansで多用されている。
オブジェクトは以下のようにシリアライズされる。
ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("SerialOutput")); oos.writeObject(someObject); oos.flush();
シリアライズされたオブジェクトは以下のように復元される。
ObjectInputStream ois = new ObjectInputStream( new FileInputStream("SerialOutput")); someObject = (SomeClass) ois.readObject();
オブジェクトのクラスがSerializableインタフェースを実装していれば、シリアライズによって、通常はアクセスできないpublicでないフィールドも含め、オブジェクトのすべてのtransientでないフィールドが保存される。シリアライズされた値の書き出されたバイトストリームが、読取り可能である場合、通常はアクセスできないフィールドの値を取得できてしまう。それだけでなく、復元されたときに不正な値になるように、バイトストリームを変更したり偽造することが可能である。
セキュリティマネージャを導入したとしても、通常はアクセスできないフィールドがシリアライズされたり復元されることを防止することはできない(バイトストリームが保存されたり送信される場合、ファイルやネットワークへの読書き権限が与えられていなくてはならないが)。しかし、SSL/TLS (Secure Sockets Layer/Transport Layer Security) を使うことで、RMI を含むネットワークトラフィックを保護することはできる。
オブジェクトのシリアライズ時や復元時に特別な処理を必要とするクラスは、以下のようなメソッドを実装することができるが、以下に示す通りのシグネチャを持っていなければならない[API 2006]。
private void writeObject(java.io.ObjectOutputStream out) throws IOException; private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
Serializableインタフェースを実装するクラスがwriteObject()をオーバーライドしていない場合、オブジェクトはデフォルトのメソッドを使ってシリアライズされる。つまり、transientではないフィールドを除くすべてのpublic、protected、パッケージプライベート、privateフィールドがシリアライズされる。同様に、Serializableを実装するクラスがreadObject()をオーバーライドしていない場合、transientであるフィールドを除き、すべてのpublic、protected、privateフィールドが復元される。この問題は「SER01-J. シリアライズに関連するメソッドは正しいシグネチャで実装する」で詳しく述べる。
並行性、可視性、メモリ
スレッド間で共有可能なメモリは、共有メモリまたはヒープメモリと呼ばれる。変数という用語は、この章では、フィールドと配列要素の両方を指す[JLS 2005]。 スレッド間で共有される変数は、共有変数と呼ばれる。すべてのインスタンスフィールド、静的フィールド、配列要素は共有変数であり、ヒープメモリに保存される。ローカル変数、メソッドの引数、例外ハンドラの引数は、スレッド間で共有されず、Javaのメモリモデルの影響を受けない。
現代の共有メモリ型マルチプロセッサアーキテクチャにおいて各プロセッサは、定期的にメインメモリと同期される複数のレベルのキャッシュを持つ(図1-3)。
図1-3. 共有メモリ型マルチプロセッサアーキテクチャ
共有変数の値はキャッシュされ、キャッシュされた値のメインメモリへの書き込みは遅延する可能性があるため、共有変数への書込みの可視性は問題となる場合がある。結果として、別のスレッドが読み取る値は最新でないかもしれない。
また別の懸念もある。コードの並行実行は一般にインターリーブされるが、それだけでなく、コンパイラやランタイムシステムがステートメントを並び替え、パフォーマンスの最適化を図ることがある。こうなると、ソースコードを見るだけでは実行順序がどうなるか分からない。実行順序が並び替えられる可能性を考慮しないためにデータ競合が発生するのは、よくあることである。
次に示す例について考えてみよう。aとbは共有グローバル変数あるいはインスタンスフィールドであり、r1とr2は他のスレッドからはアクセスできないローカル変数であると仮定する。
はじめに、a = 0、b = 0 とする。
スレッド 1 | スレッド 2 |
---|---|
a = 10; | b = 20; |
r1 = b; | r2 = a; |
スレッド1 における2つの代入(a = 10; と r1 = b;)には関連性がないため、コンパイラやランタイムシステムは実行順序を並び替えることができる。同様に、スレッド2における実行順序も自由に並び替えられる。また、直感に反するようであるが、Javaメモリモデルでは実行順で後の書込みによる値の先読みを許している。
考えられる実行順序と実際の代入の一例を次に示す。
実行順序(時間) | スレッド番号 | 代入式 | 代入される値 | 注釈 |
---|---|---|---|---|
1. | t1 | a = 10; | 10 | |
2. | t2 | b = 20; | 20 | |
3. | t1 | r1 = b; | 0 | bの初期値、すなわち0を読み取る |
4. | t2 | r2 = a; | 0 | aの初期値、すなわち0を読み取る |
この実行順では、r1とr2はそれぞれ、変数bとaの更新後の値(20と10)を読み取ることを期待されているにもかかわらず、更新前の値を読み取る。以下の表に、考えられる別の実行順序と代入の例を示す。
実行順序(時間) | スレッド番号 | 代入式 | 代入される値 | 注意事項 |
---|---|---|---|---|
1. | t1 | r1 = b; | 20 | 書込み(ステップ4)後の値(20)を読み取る |
2. | t2 | r2 = a; | 10 | 書込み(ステップ3)後の値(10)を読み取る |
3. | t1 | a = 10; | 10 | |
4. | t2 | b = 20; | 20 |
この実行順では、r1とr2は、ステップ4と3の実行結果であるbとaの値を、それらのステップが実行されるより前に読み取る。
行われる可能性のある実行順序の並び替えの候補を少なくできれば、ソースコードが正しいかどうかを判断しやすくなる。
しかし、たとえ各ステートメントがスレッドに登場する通りの順序で実行されたとしても、キャッシングのために最新の値がメインメモリに反映されないことが考えられる。
「Java言語仕様(JLS)」はJavaメモリモデル(JMM)を規定しており、プログラマにある種の保証を提供する。Javaメモリモデルは、変数の読み書き、モニタのロックと解除、スレッドの開始と合流といったプログラムの動作の観点で規定されている。Javaメモリモデルは事前発生(happens-before)と呼ばれる半順序を定義している。たとえば、動作Bを実行するスレッドが動作Aの結果を観測できることを保証するには、「AがBの前に起こる」という事前発生関係が成立しなくてはならない。
Java言語仕様 の 17.4.5節「事前発生の順序」[JLS 2005] には以下のように記されている。
- モニタのアンロックは、後に続くすべての該当モニタへのロックよりも事前に発生する。
- volatile フィールドへの書込みは、後で行われるすべての該当フィールドの読取りよりも事前に発生する。
- スレッドのstart()呼出しは、開始されるスレッド中の任意の動作よりも事前に発生する。
- あるスレッド中のすべての動作は、そのスレッドへのjoin()から正常に戻った他のスレッドよりも事前に発生する。
- オブジェクトのデフォルト初期化は、プログラムの他の任意の動作よりも事前に発生する。
- スレッドによる他スレッドへの割込みの呼出しは、割込まれたスレッドによる割込みの検知よりも事前に発生する。
- オブジェクトのコンストラクタの終了は、該当オブジェクトのファイナライザの開始よりも事前に発生する。
2つの操作の間に事前発生関係が成立しない場合、Java仮想マシン(JVM)はそれらの操作を自由に並び替えることができる。あるスレッドによって変数に値が書き込まれ、その値を少なくとも1つの別のスレッドが読み取り、それらの読み書きに事前発生関係が成立しない場合、データ競合が発生する。プログラムが正しく同期されていれば、このようなデータ競合は発生しない。Javaメモリモデルは、正しく同期されたプログラムの順序一貫性(sequential consistency)を保証する。順序一貫性とは、事前発生関係を持たない各操作をどのような順序で実行しても、すべてのスレッドによる共有データへの読み書きがある一定の順序に沿って実行され、この順序の中で各スレッドの操作が該当するプログラムに記述された順序通りに実行されたときと同じ実行結果が得られることを意味する[Tanenbaum 2003] 。これを言い換えると次のようになる。
- 各スレッドが実行する読み書きの操作を、それぞれのスレッドにおいて、実行される順序に並び替える。
- 事前発生関係を保つように、すべてのスレッドによる処理を1つの実行順序にまとめる。
- プログラムの実行が順序一貫性を持つには、読取り操作が、プログラム全体の中の最新の書込みデータを返すようにしなければならない。
- これはつまり、どのスレッドが観測する読み書き順序も同じであることを意味する。
命令とメモリアクセスの実行順序は、プログラムが実行されるたびに異なる可能性がある。これらの実行順序の並び替えは、スレッドが行う操作がプログラム全体の実行順序に沿っており、かつ、すべての値の読取りがJavaメモリモデルで許されている限りにおいて行われうる。順序一貫性のおかげで、プログラマは自分の書くプログラムのセマンティクスを理解することができ、一方でコンパイラ開発者や仮想マシンの実装者は様々な最適化を行うことができる[JPL 2006]。
Javaには、プログラマがマルチスレッドプログラムのセマンティクスを理解する上で助けとなる、並行処理に関する様々な言語要素が存在する。
volatileキーワード
共有変数を volatile 宣言することで、変数の可視性を確保しつつ、変数へのアクセス順序の並び替えを制限することができる。volatile 変数へのアクセスは、変数の値をインクリメントするといった複合操作のアトミック性を保証するものではない。したがって、複合操作のアトミック性を保証しなければならない場合、volatile 修飾だけでは不十分である。(詳細は「VNA02-J. 共有変数への複合操作のアトミック性を確保する」を参照)
変数をvolatile宣言することで、事前発生の関係が確立され、voaltile変数への書込みがその後で値を読み取るスレッドに対して可視となることが保証される。volatile変数への書込みの前に実行される各ステートメントもまた、そのvolatile変数の読取りより前に発生する。
図1-4 に示すような、複数のステートメントを実行する2つのスレッドについて考えてみよう。
図1-4. volatile 変数への読み書き操作
スレッド1とスレッド2の間には事前発生関係が成立し、スレッド2 は スレッド1が完了するまでは開始できない。
この例では、スレッド1のステートメント3がvolatile変数 v に値を書き込み、スレッド2のステートメント4がその値を読み取る。この読取り結果には、ステートメント3で変数 v に書き込まれた最新の値が反映される。
volatile変数の読み取り操作と書き込み操作は、互いに並び替えることも、volatileでない変数へのアクセスと並び替えることもできない。スレッド2がvolatile変数を読み取る時、スレッド1のvolatile変数への書込みより前に行われるすべての書込みが可視となる。このようにvolatile変数の読み書きは比較的強く保証されているため、volatile宣言をすることに伴う性能上のオーバーヘッドは、同期を行う場合とほとんど同じである。
前述の例において、ステートメント1とステートメント2がソースコードに記述されている順序で実行される保証はない。両者のステートメント間には事前発生関係が存在しないため、コンパイラは自由に並び替えることができる。
volatile変数とvolatileでない変数間で発生しうる並び替えを表1-2にまとめる。ロードとストアは、それぞれ読取りと書込みと同義である[Lea 2008]。
volatileキーワードがもたらす可視性と並び替えに関する保証は、変数のみに適用されることに注意。つまり、これらの保証はプリミティブフィールドやオブジェクト参照だけに適用されるということである。この保証の対象となる実際のメンバフィールドは、オブジェクト参照自身であって、volatile宣言されたオブジェクト参照によって指されるオブジェクト(リファレント)はこの保証の範囲外である。したがって、参照されるメンバに対する変更を可視にするには、オブジェクト参照をvolatile宣言するだけでは不十分である。つまり、他のスレッドからそのようなリファレントのメンバフィールドに書き込まれた最新の値を、スレッドは観測できないかもしれないということである。その上、リファレントが可変でありかつスレッドセーフでないならば、他のスレッドは、構築が完了していないオブジェクトや、矛盾した状態にあるオブジェクトを観測する恐れがある[Goetz 2007]。しかし、リファレントが不変である場合、オブジェクト参照をvolatile宣言するだけでリファレントのメンバの可視性を保証することができる。
表1-2. volatile変数とvolatileでない変数間の並び替え
並び替えが可能 | 2番目の操作 | |||
---|---|---|---|---|
最初の操作 | 通常のロード | 通常のストア | Volatileロード | Volatileストア |
通常のロード | 可 | 可 | 可 | 不可 |
通常のストア | 可 | 可 | 可 | 不可 |
Volatileロード | 不可 | 不可 | 不可 | 不可 |
Volatileストア | 可 | 可 | 不可 | 不可 |
同期
正しく同期されたプログラムとは、順序一貫性を保って実行したときにデータ競合が発生しないものを指す。次に示す例はvolatileでない変数xとvolatile変数yを使用しているが、正しく同期されていない。
スレッド1 | スレッド2 |
---|---|
x = 1 | r1 = y |
y = 2 | r2 = x |
この例には、次に示す2通りの順序一貫性を持つ実行順序が考えられる。
ステップ (時間) | スレッド番号 | 式 | コメント |
---|---|---|---|
1. | t1 | x = 1 | volatileでない変数への書込み |
2. | t1 | y = 2 | volatile変数への書込み |
3. | t2 | r1 = y | volatile変数の読取り |
4. | t2 | r2 = x | volatileでない変数の読取り |
および
ステップ (時間) | スレッド番号 | 式 | コメント |
---|---|---|---|
1. | t2 | r1 = y | volatile変数の読取り |
2. | t2 | r2 = x | volatileでない変数の読取り |
3. | t1 | x = 1 | volatileでない変数への書込み |
4. | t1 | y = 2 | volatile変数への書込み |
前者の実行順序においては事前発生関係が成立し、ステップ1とステップ2は必ずステップ3とステップ4より前に実行される。しかし、後者の実行順序においては、いかなるステップ間にも事前発生関係が成立しない。したがって、データ競合が発生する。
正しく可視性を確保すると、共有データにアクセスする複数のスレッドが互いの処理結果を観測できることが保証されるが、スレッドがデータにアクセスする順序は一意には定まらない。一方、同期を正しく行うことで、可視性だけでなく、スレッドが適切な順序でデータにアクセスすることも保証できる。たとえば、次に示すコードにおいて、順序一貫性を持つ実行順序はただ1つだけとなり、スレッド2が動作する前にスレッド1のすべての処理が実行される。
class Assign { public synchronized void doSomething() { Thread t1 = new Thread() { public void run() { // スレッド1の処理を行う x = 1; y = 2; } }; Thread t2 = new Thread() { public void run() { // スレッド2の処理を行う r1 = y; r2 = x; } }; t1.start(); t2.start(); } }
同期処理を行う場合、変数yをvolatile宣言する必要はない。同期処理では、ロックの確保、各操作の実行、およびロックの解放を行う。上記の例では、doSomething() メソッドがAssignクラスのオブジェクトの固有ロックを取得している。ブロック同期を用いて以下のようにコーディングしてもよい。
class Assign { public void doSomething() { synchronized (this) { // ... doSomething() 本体の残り部分 ... } } }
これら2つのコード例で使っている固有ロックは同じものである。オブジェクトの固有ロックはモニタと呼ばれることもある。オブジェクトの固有ロックは、その解放と次の取得との間に、事前発生関係が必ず確立される。
アトミッククラス volatile変数の使用は、可視性の保証には有効であるが、アトミック性の保証には不十分である。同期メカニズムを使用することでこの問題を解決することはできるが、逆に、コンテキストスイッチのオーバーヘッドを招き、ロック競合が発生することが少なくない。java.util.concurrent.atomicパッケージのアトミッククラスを使えば、実環境で発生する多くの競合を減らし、かつ、アトミック性を保証することができる。Goetzらは次のように述べている。「争奪のレベルがそれほど高くないときは、アトミック変数のスケーラビリティが優位です。争奪のレベルが高いときは、ロックの争奪回避の能力が優れています。」[Goetz 2006a]。
アトミッククラスは、今日のプロセッサが提供するコンペア・アンド・スワップ命令の強みを生かして効率的なプログラム実行を提供しつつ、プログラマが一般に必要とする機能を提供する。たとえば、AtomicInteger.incrementAndGet()メソッドは、変数をアトミックにインクリメントする機能を提供する。ほかにも、java.util.concurrent.atomic.Atomic*.compareAndSet()(*にはInteger、Long、Booleanなどが入る)のような高水準メソッドは、プロセッサの機能を活用しつつ明快な抽象インタフェースをプログラマに提供する。
synchronizedキーワードやvolatile変数といった従来の同期プリミティブよりも、java.util.concurrentユーティリティの利用が好まれる。その理由は、これらのユーティリティが同期メカニズムの実装の詳細を抽象化し、より簡潔で誤用しにくいAPIを提供することで、スケーラビリティに優れ、ポリシーに基づいた同期の使用を可能にするからである。
Executorフレームワーク java.util.concurrentパッケージは、Executor フレームワークを通じて、タスクを並行実行する仕組みを提供する。タスクとは、RunnableあるいはCallableインタフェースを実装するクラスによってカプセル化された、論理的な作業単位を指す。Executorフレームワークは、低レベルのタスクスケジューリングやスレッド管理の詳細から、タスクをリクエストする操作を分離する。さらにこのフレームワークは、システムが同時に処理できる数以上のリクエストを受けた場合でも、一定の機能を提供し続けることができるスレッドプールの仕組みを提供する。
フレームワークの核となるのは、Executor インタフェースである。このインタフェースは、スレッドプールの終了やタスクの返り値(Futureインタフェース)の取得を行うExecutorServiceインタフェースによって拡張される。ExecutorServiceインタフェースはさらに、タスクを定期的にあるいは遅れて実行する機能を提供するScheduledExecutorServiceインタフェースによって拡張される。Executorsクラスは、ファクトリメソッドやユーティリティメソッドを持つが、これらのメソッドは、Executor、ExecutorService、およびその他の関連するインタフェースが共通して使用する設定を提供する。たとえば、Executors.newFixedThreadPool()メソッドは、同時に実行できるタスクの数に上限が設けられたスレッドプールを返り値として返し、スレッドプールが満杯の間はタスクを保持するための無制限キューを維持する。スレッドプールの基本的な実装は、ThreadPoolExecutor クラスが提供する。このクラスのインスタンスを作成し、タスクの実行ポリシーをカスタマイズすることもできる。
明示的ロック java.util.concurrentパッケージの、ReentrantLockクラスは、固有ロックにはない機能を提供する。たとえば、ReentrantLock.tryLock()メソッドは、他のスレッドが既にロックを保持している場合に直ちに呼び出し元へ戻る。Java メモリモデルにおける ReentrantLock の取得と解放のセマンティックスは、固有ロックの取得と解放のセマンティクスと同じである。
最小権限の原則
最小権限の原則によると、すべてのプログラム、システムのすべてのユーザは、あるタスクを完了するために必要な最小限の権限を持って動作すべきである[Saltzer 1974, Saltzer 1975]。"Build Security In"のウェブサイト[DHS 2006]はこの原則に別の定義を与えている。最小限の権限でプログラムを実行することで、コードに脆弱性が見つかった場合の攻撃の深刻度を低減できるだろう。
最小権限の原則を課すルールには以下が存在する。
- 「ENV03-J. 危険な組み合わせのパーミッションを割り当てない」
- 「SEC00-J. センシティブな情報を特権ブロックから信頼境界を越えて漏えいさせない」
- 「SEC01-J. 汚染された変数を特権ブロックの中で使わない」
セキュリティポリシーで複数のパーミッションを設定する場合は、必要なものに限定すべきである。セキュリティマネージャを持つJavaのプログラムを実行すると、デフォルトのセキュリティポリシーファイルは控えめなパーミッションをプログラムに与える。しかし、Javaのセキュリティモデルは柔軟であり、ユーザは独自にセキュリティポリシーを定義することで、プログラムに更なるパーミッションを許可することができる。
Javaでは、コードに特権を与えるにはコード署名が必要である。セキュリティポリシーの多くは、署名されたコードが特権で動作することを許可する。特権を必要とするコードのみに署名すべきであり、その他のコードは署名すべきでない(「ENV00-J. 特権の必要ない動作のみを行うコードを署名しない」を参照)。
同一のJARファイルの中で、署名が必要なコードが署名のないクラスと混在する場合がある。しかし、特権が必要なコードはひとつのパッケージにまとめることが推奨される(詳細は「ENV01-J. セキュリティ上重要なコードは署名付きの1つの JAR にまとめてシールする」を参照)。また、セキュリティポリシーを使用し、コードベースあるいは署名者ごとにコードに権限を与えるという方法もある。
特権を持つ動作は、そのような特権を必要とする最小限のコードブロックに限定すべきである。JavaのAccessControllerの仕組みを用いれば、コードの特定の部分だけに特権を持たせることができる。クラスが特権を行使する必要がある場合、doPrivileged()ブロックのなかで特権を持つコードを実行する。AccessControllerの仕組みは、設定が有効なセキュリティポリシーと組み合わせて動作する。プログラムのユーザは、Javaのセキュリティモデルの詳細を知らず、自らが求める要件を満たすようなセキュリティポリシーを正しく設定できないかもしれない。脆弱性を避けるためにも、doPrivileged()ブロックの中に書く特権を持って動作するコードは最小限にしなくてはならない。
セキュリティマネージャ
SecurityManagerは、Javaのコードに対してセキュリティポリシーを定義するクラスである。セキュリティマネージャをインストールせずに実行されるプログラムには、セキュリティ上の制限は課されない。つまり、JavaのAPIが提供するすべてのクラスやメソッドを使用することができる。セキュリティマネージャが存在する場合、潜在的に安全でない、センシティブな動作のうち、どれが許されるかを指定する。セキュリティポリシーが許可しない動作を行おうとすると、SecurityExceptionがスローされる。コードは、どの動作が許可されるかをセキュリティマネージャに問い合わせることができる。また、セキュリティマネージャは、信頼されたJava APIに対しても、どの機能を実行できるかを制御することができる。信頼できないコードのシステムクラスへのアクセスを許可しないのであれば、そのようなコードには限られたパーミッションのみを許可し、特定のパッケージの中にある信頼されたクラスがアクセスされるのを防ぐべきである。accessClassInPackageパーミッションはそのような機能を提供する。
ある種のアプリケーションでは、予め定義されたいくつかのセキュリティマネージャを利用することができる。たとえば、すべてのJavaアプレットは、アプレットセキュリティマネージャで管理されている。このセキュリティマネージャは、最低限必要な権限を除くすべてのパーミッションを拒否し、意図しないシステムの改変、情報漏えい、なりすましが行われないように設計されている。
セキュリティマネージャの使用は、クライアント側の保護に限られているわけではない。TomcatやWebSphereといったwebサーバも、セキュリティマネージャの機能を使用し、トロイの木馬を意図したサーブレットや悪意ある Java Server Page (JSP) コードをプログラム自身から分離したり、センシティブなシステムリソースを意図に反したアクセスから保護する。
コマンドラインで実行されるJavaのアプリケーションは、フラグを指定することで独自のセキュリティマネージャを有効にすることができる。プログラムの中でセキュリティマネージャをインストールするコードを書くことももちろん可能である。この仕組みは、セキュリティポリシーに基づいてセンシティブな動作を許可したり拒否したりするデフォルトのサンドボックスの作成を可能にする。
Java 2 SE プラットフォームより前のSecurityManagerクラスはabstractクラスであった。現在はabstract宣言されていないため、メソッドを明示的にオーバーライドしなくてもよい。プログラム中でセキュリティマネージャを作成し、使用するには、そのコードは実行時パーミッションであるcreateSecurityManagerを持っていることが必要であり、SecurityManagerクラスのインスタンスを作成し、これをsetSecurityManagerメソッドを使ってインストールする。これらのパーミッションは、セキュリティマネージャが既にインストールされている場合にのみ、チェックされる。このような仕組みは、たとえば仮想ホストのように、システム全体にデフォルトのセキュリティマネージャが存在し、各ホストが、デフォルトのセキュリティーマネージャを独自のものにオーバーライドすることを認めないようにする場合に有用である。
セキュリティマネージャはAccessControllerクラスと密接に関連している。前者がアクセス制御の拠点として使用されるのに対し、後者はアクセス制御アルゴリズムの実装を提供する。セキュリティマネージャは以下のことを行う。
- 後方互換性の提供。SecurityManagerが元々abstractクラスであったため、レガシーコードには独自に実装されたセキュリティマネージャが含まれることが多い。
- 独自のポリシーの定義。セキュリティーマネージャのサブクラスを作成することで、独自のセキュリティポリシーを定義することができる(複数の階層におよぶもの、粗いポリシー、細かいポリシー、など)。
デフォルトのセキュリティマネージャではなく、独自のセキュリティマネージャを実装し、使用することに関して、Java 2 プラットフォームのセキュリティーアーキテクチャー[SecuritySpec 2008]には次のように書かれている。
アプリケーションコードでAccessControllerを使うことを強くお勧めします。一方、セキュリティマネージャーの(サブクラス化による)カスタマイズは、細心の注意を払って行う必要があるため、最後の手段にすべきです。さらに、標準セキュリティーチェックを呼び出す前に常に日時をチェックするようなカスタマイズされたセキュリティーマネージャーには、適切な場合にはいつでも AccessController が提供するアルゴリズムを使用すべきです。
Java SE の APIの多くは、デフォルトの動作として、センシティブな処理を実行する前にセキュリティマネージャによるチェックを行う。たとえば、java.io.FileInputStreamクラスのコンストラクタは、呼び出し元がファイルの読み取り権限を持たない場合、SecurityExceptionをスローする。SecurityExceptonはRuntimeExceptionのサブクラスであるため、RuntimeExceptionをスローすることを宣言する必要がない。実際、APIのメソッドによっては、宣言していないクラスも存在する。たとえば、java.io.FileReaderクラスの宣言には、throws SecurityExceptionが存在しない。APIのマニュアルに、メソッドがセキュリティマネージャによるチェックを行うかどうかが明記されていないのであれば、チェックの有無の影響を受けるコードを書くべきではない。
クラスローダ
java.lang.ClassLoaderクラスとそのサブクラスは、JVMに動的にクラスをロードする仕組みを提供する。あらゆるクラスは、そのクラスをロードしたClassLoaderへのリンクを持つ。また、あらゆるクラスローダはそれをロードした親のクラスローダを持ち、最終的に root となるクラスローダに行き着く。ClassLoaderは抽象クラスであるため、そのインスタンスを作成することはできない。すべてのクラスローダはSecureClassLoaderから継承し、また、SecureClassLoader は ClassLoaderから継承している。SecureClassLoaderクラスは、自身のメソッドおよびサブクラスのメソッドに対してセキュリティチェックを行う。SecureClassLoaderクラスが定義するgetPermissions()メソッドは、クラスローダによってロードされたクラスに割り当てられているパーミッションを返す。このメソッドを使うことで、信頼できないコードによって新たにロードされたクラスを制限することができる。
幸い、異なるクラスローダによってロードされたクラスはすべて異なるものとして扱われる。信頼できないコードに対するセキュリティという観点では、(デフォルトの)パッケージプライベートのアクセス範囲は、private 宣言と同じアクセス範囲を意味していると考えることができる。
まとめ
Javaは比較的セキュアな言語ではあるが、それでも、システムを攻撃の危険に晒す様々なプログラミングエラーを引き起こしやすい。一般的なプログラミングエラーを防ぐためにJavaが提供する機能を使えば、プログラムが本質的にセキュアになりそれ以上何かする必要はない、と考えるのは最も大きな間違いである。些細なバグであっても、セキュリティに甚大な悪影響を及ぼす可能性があるため、脆弱性のないシステムの開発と実運用には、常にセキュリティの視点に立って考えることが不可欠である。
コーディングエラーが脆弱性を引き起こす可能性を低減するためにも、Javaプログラマは、本コーディングスタンダード等が定めるルールに従ってコーディングすべきである。
翻訳元
これは以下のページを翻訳したものです。
Introduction (revision 54)