本サイトは、快適にご利用いただくためにクッキー(Cookie)を使用しております。
Cookieの使用に同意いただける場合は「同意する」ボタンを押してください。
なお本サイトのCookie使用については、「個人情報保護方針」をご覧ください。

最新情報

2023.03.02

セッションのレースコンディション(1)

主にWebアプリケーションスキャナを開発している寺田です。

本日はJavaのWebアプリケーションにおけるセッションのレースコンディションについて書きます。StrutsやSpringのセッションスコープのフォームを使用しているアプリケーションに影響しうる問題ですので、該当する方は参照ください。

要約

  • Java Servletのセッションは、同じセッションIDのリクエストを複数同時に処理する際にレースコンディション問題を起こしうる仕組みになっている。
  • Struts1/2, Springのセッションスコープのフォームを使っていると、レースコンディションによりフレームワークやアプリケーションによるチェックをバイパスされるおそれがある。
  • Synchronizedブロックによるロック等の回避策がある。

Java Servletのセッションが持つ特性

Java Servletのセッションは、他の処理系のセッションとは違う特殊な性質を持っています。どういうことなのか下の図で見てみましょう。図のリクエストA,Bは同じセッションIDを持つリクエストで、これらがほぼ同時にサーバに送られたとします。

sequence_concept.png

①A側においてセッション変数にAAAという値を入れ、②その直後にB側でBBBという値を入れています。③この後にA側で値を参照すると、AAAではなくBBBになってしまいます。

このような現象が発生するのは、セッションオブジェクトがServletコンテナのメモリ上に保持されており、同じセッションIDに対するセッションオブジェクトはリクエストをまたいで同じインスタンスになるためです。同じインスタンスに複数のリクエスト(スレッド)からアクセスしている訳で、典型的なマルチスレッドにおけるレースコンディション問題が発生しうる状況と言えます。

このことは下のようにServletの仕様にも書かれています。

Multiple servlets executing request threads may have active access to the same session object at the same time.
Servlet Specification 6.0 - 7.7.1. Threading Issues

実際、筆者が検証環境(Tomcat, Jetty, WebLogic)にテスト用のServletを置いて試した際も、上のような値が入れ替わる挙動は割と簡単に再現できました。

上図では左右からリクエストがサーバに届いていますが、作図の都合によるものです。ネットワーク的には同じインターネット側から2つのリクエストが届くと考えてください。他の図も同じです。 HttpSessionがスレッドセーフでないことについては、「java session thread safe」等のキーワードでネット検索すると日本語の情報も若干出てきます。

セキュリティへの影響

この挙動を悪用すると、以下のような攻撃が可能である場合があります。下のリクエストA,Bも同じセッションIDを持つリクエストです。

sequence_attack.png

アプリケーションとしては、チェック(②)した値を使用(③)しているつもりですが、リクエストBの①'が②と③の間に実行されれば、チェック後の値を書き換えられてしまいます。要はTOCTOU(Time of check to time of use)問題により、チェックを回避される可能性があるということです。

攻撃のモデルは主に「能動的な攻撃」になります。つまり図のリクエストA,Bはともに攻撃者自身が自分のセッションを使ってサーバに送信します。他人に同時複数のリクエストを送らせてXSS等につなげる「受動的な攻撃」も考えられなくはないですが、こちらはかなりやりづらい攻撃になると思われます。

ところで、上のアプリケーションは、セッションに値を保存してから、その後にその値をチェックしています。読者の方は「通常のWebアプリケーションは、チェックOKだった値だけをセッションに保存するのではないか?」と思われるかもしれません。確かにそうですが、次節でみるように、Webアプリケーションフレームワークの中にはフォームの処理において上の①②③のような処理をするものがあります。

Javaのフレームワークの挙動

上のようなセッションに関わるTOCTOU問題が発生しうるのは、筆者が調べた限りではStruts1/2, Springの3つのフレームワークです。

これらのフレームワークには(ものにより用語やコンセプトが若干違いますが)以下の機能があります。

  • Populate
    リクエストパラメータをForm Bean(以下Beanと呼ぶ)のプロパティに割り当てる
  • Validate
    Beanのプロパティをチェックする
  • Controller
    チェックOKならControllerの実行メソッドにBeanを渡す

さらに以下の機能もあります。

  • Beanをセッションに保存する
    セッションスコープのBean

セッションスコープのBeanでは、下のように前節の①②③に合致する状況になります。チェックしたものをセッションに入れるのではなく、セッションに入れたものをチェックする形です。

Populate①入力値をセッションに保存
Validate②セッションの値をチェック
Controller③セッションの値を使う

検証と分析

上記の3フレームワークで、セッションスコープのBeanを使ったアプリケーションを作り、TOCTOU問題が実際に発生することを検証しました。Beanをセッションスコープにする設定は以下の方法で行いました。

Struts1XMLでのAction定義時にscope=sessionを指定
Struts2ControllerのInterceptor設定でscopedModelDriven.scope=sessionを指定
SpringControllerに@SessionAttributesを指定

その結果、いずれのフレームワークにおいても、リクエストを複数同時に送ることによって、フレームワークによるチェックを回避できてしまいました。つまりチェックNGとなるBeanがControllerのメソッドに渡される可能性があります。

下のStruts1のコードで言うと、form(ActionForm Bean)がセッションに入れられ、リクエストをまたいで同じインスタンスになります。このコードではBeanのemailプロパティを2回参照しており(email1, email2)、フレームワークのバリデーション設定を行っていれば、本来は両方とも同じチェックOKの値が得られるはずです。しかし、同時に送るリクエストにより、email1をチェックNGとなる値にしたり、email1はOKでemail2をNGとなる値にしたり、ということができてしまいます。

struts1_action_sample.png

上の例でemail1とemail2が違う値になりうるということは、Controllerの中でもBeanのプロパティが変化するということです。これは、フレームワークがControllerの手前で行う構文チェックだけでなく、アプリケーションがControllerで行う認可制御等を含むチェックも回避されうることを示唆しています。もちろん、アプリケーションのチェックを回避できるかは処理の仕方次第ですし、フレームワークやアプリケーションのチェックを回避できたとしてそれが意味のある攻撃につながるかはデータの使い方次第ですが、Beanの中身が固定されていないのは潜在的に危険だとは言えます。

3フレームワークのうち、Springにはこのような問題に対処する「synchronizeOnSession」(以下SOS)という設定があります。デフォルトでは無効になっていますが、これを有効にすると、上記のPopulateからControllerまでがsynchronizedブロック内(セッション単位)で実行されるようになり、同じセッションIDを持つリクエストの処理が複数同時に走ることはなくなります。しかしControllerのあと、つまりViewはsynchronizedのスコープ外になるため、ViewでBeanを参照すると、同時に送信される他のリクエストにより書き換えられた値が得られる可能性があります。

上のコードの中でformがリクエストをまたいで同じインスタンスになるのは、Populate時に既にセッションにBeanが存在する場合、フレームワークは新たなBeanインスタンスを作成せずに、既存のBeanインスタンスに対してプロパティをセットしていくためです。Struts1に限らず、この手のフレームワークの多くはそのような仕様で作られていると思います。

再現性

診断でみる一般的なレースコンディションバグの多くは、1~2回の試行で再現します。しかし、上記のセッションに対するレースコンディションについては、TOC(Time of check)とTOU(Time of use)の間の時間(クリティカルセクション)が短いことが多いために、それよりも再現性は低いと思います。

再現率の例を挙げると、筆者がStruts1のアプリケーションを対象に実施したテストでの再現率は15.5%でした(攻撃成功 1,551回 / 総試行回数 10,000回)。このテストは、AWSのEC2インスタンス(t3.medium)上のTomcatに検証用のアプリケーションを置き、弊社のネットワークからインターネット越しにアクセスして、フレームワークによる構文チェックの回避を試みたものです。

Struts1、Struts2、Spring(SOS無し)のいくつかのアプリケーションで同様のテストを行った際の再現率は0.5%~20%程度でした。再現率は、アプリケーション、フレームワーク、ネットワーク、ハードウェア、ソフトウェアといった環境やテスト方法にも左右されますが、大雑把に言うと、早ければ10回で、そうでないものは100回、1,000回とテストすれば再現できるだろう、というレベル感です。

この程度の再現率だと、おそらく攻撃はツールを使って自動化する必要があり、またツールは個々のアプリケーション向けにカスタマイズしたものが必要になります。つまり攻撃のコストは高い訳ですが、さりとて「非現実的な攻撃なので、完全に無視してよい」とも言い難いと筆者は考えています。

脆弱性の届け出

前述のとおりHttpSessionがスレッドセーフでないことはServletの仕様です。Servletを使うアプリケーションの開発者はそのことを考慮して開発する必要がありますが、フレームワークを使う開発においてはフレームワークがそういった問題を肩代わりしてくれくれるのが理想だと思います。

そこで、筆者は上記の3つのフレームワークの挙動をフレームワークの脆弱性としてIPAに届け出しました(2019年2月)。その後3年以上を経過しても修正されなかったため、2022年12月に脆弱性関連情報の非開示依頼の取下げの申請を行い、2023年1月末~2月初に取り下げが完了しました。公開して対処を促す方が望ましいとの考えに基づき、このブログ記事にて開示を行っています。

それぞれのフレームワークについて、IPAへの届け出と現在の状況を簡単にまとめます。

Spring

Springについては、前述のとおり、SOS(synchronizeOnSession)を有効にしていたとしても、チェックをパスしないBeanがViewの中で得られてしまう可能性があることを届け出ました。この点について現在は以下のドキュメントに記述されています。

TERASOLUNA Server Framework for Java (5.x) Development Guideline (5.7.1.SP1.RELEASE)
Macchinetta / server-guideline-thymeleaf · GitHub

IPAを介した開発者側とのやり取りの中で、SOSでできることを明確化するという趣旨の話があり、それに沿ってドキュメントに記述が追加されたのだと思います。ドキュメント化されたというのは、修正されずに「仕様」になってしまったということではありますが、アプリケーション開発者が知る機会があるという意味では少し前進したと捉えてます。

Springにおける回避策は、まずはSOSを使うことと、そしてSOSを有効にしてもViewで参照するBeanは未チェックの値を持ちうるため、純粋に表示するだけの用途に留めておくか(Viewなので普通はそうですが)、変化しない状態にした上で再度チェックしてから使う、ということになるかと思います。

Struts1/2

Struts1/2にはsynchronizeOnSessionのような仕組みがなく、Controller等においてフレームワークのチェックをパスしないBeanを使ってしまう可能性があり、このことを脆弱性として届け出ました。

現在までに修正等は行われていません。指摘した点について、開発者側は「フレームワークの脆弱性でない」との見解のようです。筆者が理解したところの開発者側の認識は、セッションに同時アクセス制御が無いことはServletの仕様でありアプリケーション開発者はそれを認識すべきである、またトランザクショントークン(ワンタイムトークン)で対処すべき、というものです。

Struts2について言えば、確かにワンタイムトークンで攻撃を回避できる場合もありますが、それは一定の条件が満たされるときに限られます。詳細は補足1を参照していただくとして、Struts2については一定の条件を満たした上でワンタイムトークンを使うというのが、回避策であると思います。Struts1については、後述する別の回避策を取るしかないと思います。

影響

影響を受けるのは、以下の条件を全て満たすアプリケーションです。

① Struts1/2, SpringフレームワークでセッションスコープのBeanを使っている。
 (OR カスタムのJavaアプリケーションで同様の処理をしている)。
② リクエストをまたいでセッションオブジェクトが同じインスタンスになる環境である。
③ Struts2においては、トークンチェックの設定等が一定の条件を満たしていない。

②で言っているのは、セッションオブジェクトがリクエスト毎に異なるインスタンスになる環境も存在し(例: 補足2に書いたSpring Session)、そういった環境では影響を受けないということです。また、①のとおり、セッションを使っていたら必ず影響を受ける、ということではありません。例えばログインしたユーザ情報の保持にセッションを使っていたとしても、セッションスコープのBeanを使っていないならば影響を受けません。③については補足1を参照ください。

次に被害についてです。直接的な被害は、①に該当する箇所におけるチェックの回避です。フレームワークによる構文チェックと、アプリケーションによる各種のチェックの両方が回避されるおそれがあります。アプリケーションによるチェックが回避されうるか、あるいはフレームワークやアプリケーションによるチェックを回避された時に生じる被害は、アプリケーションの作りやデータの使い方により異なります。

SpringのsynchronizeOnSessionを有効にしている場合は、Viewにおいて未チェックの値を使ってしまうというのが直接的な被害です。この場合は、Viewで何をしているか次第ではあるものの、synchronizeOnSessionが無効になっている時と比べて深刻な被害につながる可能性は低くなると思います。

別の回避策

前述のようにSpringやStruts2については一応の回避策があります。しかし、Struts1には回避策がありませんし、SpringやStruts2についても別の回避策が必要なこともあると思うので、以下に筆者が考える回避策について書きます。

回避のアプローチは2つあります。2つは、攻撃の前提である「同一のインスタンス」への「同時アクセス」にそれぞれ対処するものです。

同一インスタンス

同一インスタンスになることを防ぐ回避策としては、補足2に書いたSpring Sessionの使用や、セッションに保存されているオブジェクトをServletFilter等でディープクローンする方法が考えられます。Spring Sessionを使った回避策については補足3に留意点を記載しました。

同時アクセス

SpringのsynchronizeOnSessionと同じく、ロック(synchronized)により同時アクセスを防ぐアプローチです。インスタンスを別にするアプローチと比べて導入しやすいというメリットがあるので、まずはロックによる回避策の導入を検討するのがよいというのが筆者の考えです。次節でこの回避策について説明します。

注意 記事本文や補足の中で回避策をいくつか紹介していますが、アプリケーション・フレームワーク・コンテナのコードに手を入れずに、ツールやServletFilterで簡単に何とかならないか、という方向性で検討したものです。フレームワークやコンテナがお膳立てする根本的な対策ではないため、不完全な部分がある旨をご承知ください。いずれの回避策を採るにしても、機能と性能の両面で問題が生じないかテストすることをおすすめします。なお、アプリケーションの改修をいとわないのであれば、そもそもBeanをリクエストスコープにすれば問題は生じません

ServletFilterでディープクローンをする場合、session.setAttribute(name, deepcopy(session.getAttribute(name));のような処理を全ての属性に対して実施する方法が思いつきますが、それでは不十分です。requestsessionをラップして挙動を変える必要があると思われます。

Synchronizedによるロック

一般にロックの範囲は狭いほどよいですが、セッションにはアプリケーションの大部分からアクセスする可能性があるため、範囲を狭めるのは容易ではありません。ここではServletFilterでロックする前提で考えるので、範囲を狭めることはできず、リクエストを受けてからレスポンスを返すまでの全ての処理をロック対象にすることになります。

ServletFilterの一番単純な実装は以下です。

lock_servlet_filter.png

ロック方法については補足4も参照ください。

このように、synchronizedブロックの中でリクエストを処理すると、Webクライアントから同じセッションIDのリクエストを複数同時に受けたとしても、下図のように1つずつ処理が行われるはずです。

sequence_lock.png

ちなみに、PHPやASP.NETのデフォルトのセッション機構は、同様のロック処理を自動でしてくれます。以下はPHPマニュアルの引用です。

セッションデータは、同時書き込みを防ぐためにロックされるため、ある時点であるセッションの処理ができるスクリプトは、1つだけです。セッションでフレームセットを使用する場合、このロックのためにフレームがひとつずつロードされるような経験をするでしょう。
PHP: session_write_close - Manual

ただし、以下でみるように考慮すべきことがあります。

[A] パフォーマンス/UXへの影響

1つ目はパフォーマンスやUXへの影響です。サーバ側の処理負荷もありますし、上に引用したPHPマニュアルのとおり、動的に出力するフレーム、画像、Ajax等がひとつずつロードされることになり、ページのロード完了までの時間が長くなることが懸念されます。特に、長い時間がかかる処理が実行中の時は、同じセッションIDの別のリクエストが完全にブロックされてしまいます。

これが問題になる場合は、リクエストのURL pathをもとに、ロック不要な機能(セッションスコープのBeanを使わない機能)をロックの対象外にすることで、UXへの影響を緩和できる可能性があります。

ロック対象にするかをURLのpathを元に判定する場合は、URLを細工してロックの回避を試みるような攻撃に注意が必要です。一般的には、Allowlist的な判定、つまりロック対象外とする安全なpathを定義し、それに基づいてロック対象か否かを判定した方が安全です。例はGistに載せたServletFilterの実装例にあります。

[B] ロックがうまく機能しないケース

考慮すべき点の2つ目は、ロックがうまく機能しない場合があることです。例えば、下図のようにControllerの中から別スレッドを立ち上げており、別スレッドの終了を待たずにメインスレッドの処理を先に進めてしまう時に発生します(Servlet 3.0以降の非同期処理を使う時にも発生します)。

sequence_lock_thread_overrun.png

メインスレッドのsynchronizedブロックが終了する時にAのロックが外れるため、その後に別スレッドの中でBeanを参照すると、リクエストBにより変更された値を読んでしまいます。

これに対処するには、Aがロックを保持している間にBeanをディープクローンしておき、別スレッドではそれを参照するようにすれば問題ありません。ただしクローンしたBeanに書き込んでもクローン元のオリジナルには反映されないため、ReadOnlyなBeanとして使用することになります。

SpringのsynchronizeOnSessionも仕組みは同じなので、別スレッドでは汚染された値を読んでしまう可能性があります。 ロックがうまく機能しないケースは他にも考えられます。分かりやすいのは、ServletFilterを経由せずにセッションが参照・更新されるケースです。例えば、WebSocketアプリケーションがHttpSessionにアクセスしている場合や、複数コンテナがある環境において他のコンテナから来たレプリケーションの同期によりセッションが更新される場合が考えられます。重箱の隅をつつけば他のケースもあるかもしれません。


補足1 ワンタイムトークンによる回避策

Struts1/2のワンタイムトークンは、フレームワークの内部のsynchronized内でチェック・更新されます。ということは、1つのコンテナに対して正しいワンタイムトークン付きのリクエストを複数同時に送っても、そのうち1つのリクエストしか正常に処理されません。なので「ワンタイムトークン付きのリクエストであれば、セッションのレースコンディションを突く攻撃はできないのではないか?」と思われるかもしれませんが、話はそんなに単純ではありません。

長いので先に書くと、Struts1=ワンタイムトークンを使っていても攻撃できる、Struts2=設定等によって攻撃を回避できる場合/できない場合がある、という結論です。

説明のため、ユーザがメールアドレス(email)を登録するためのフォームを例にします。このフォームの処理は以下の4つのリクエストから成ります。メールアドレスは②のリクエストでクライアントからサーバにPOSTされてセッションに保存され、③でDBに保存されます。

リクエスト メソッド等 パラメータ 備考
①入力 GET
②確認 POST email=a@mbsd.jp セッションに保存
③登録 POST, リダイレクト token=.... DBに登録
④完了 GET

攻撃は、正常なリクエスト①②を順に送ったのちに、正常なトークンを含む③のリクエストを送る段階で、セッションを汚染する役割の②のリクエスト(不正なemailをPOSTする, トークンを含まない)を同時に送るという形になるでしょう(下図)。

①入力
 ↓
②確認
 ↓
(同時送信) ③登録 + ★汚染する役の②

③の内部処理では、セッション内のBeanのチェックを行い、OKであればDBに登録します(下図)。

③登録
セッション内のBeanのチェック
↓             ⇐ ★汚染する役の②のPopulate処理が走る
DBに登録

③のDB登録の直前に、汚染する役の②のPopulate処理が実行されて、本来はチェックではじかれるべき不正なメールアドレスがDBに登録されたら、攻撃成功とします。

この例では上記①~④のリクエストのうちトークンが要求されるのは③だけであり、汚染する役の②のリクエストにはトークンは要求されません。同時に送るリクエストのうちトークンが必要なものは1つだけなので、ワンタイムトークンにより攻撃が妨げられることはありません。

アプリケーションによっては②のリクエストにもトークンが要求されますが、その場合にも以下のA,B,Cの攻撃方法が考えられます。

[A] 別のリクエストを汚染する役にする

汚染する役を②以外のリクエストに担わせます。アプリケーション次第ですが、例えば①のリクエストにemailパラメータを追加してやると、その値がセッションに保存されることもあります。通常、①のGETのリクエストにはトークンは要求されないので、トークンが不要な①と、トークンが必要な③の組み合わせで攻撃できることもあります。

[B] Populateとトークンチェックの順序を利用する

Struts1/2のアプリケーションでは、Populateの後にトークンチェックという順番で処理が行われることがあります。この場合、汚染する役である②のリクエストにトークンが付いておらずエラーになるとしても、トークンエラーになる前にPopulateが行われるために、セッションのBeanを汚染する役目は果たしてくれます。そうであれば②にトークンチェックがあるかないかは関係なく、トークンを含まない②のリクエストを汚染する役として、③と共に送信してやればよいことになります。

鍵となるのはPopulateとトークンチェックの順番ですが、これはフレームワークや設定によって異なります。

Struts1

Struts1のトークンチェックは、Controller(Action)の中でアプリケーションがするものです。PopulateはControllerより前にフレームワークが行うため、常にPopulateの後にトークンチェックという順で処理が行われます。

Struts2

PopulateはParamsInterceptorで行われ、トークンチェックはTokenInterceptor(またはTokenSessionStoreInterceptor)で行われます。Interceptorの実行順序は設定次第ですが、ParamsInterceptorはデフォルトのInterceptorスタックに含まれており、これにTokenInterceptorを後付けする形の設定をすることも割とあるでしょう。その場合はPopulateの後にトークンチェックが実行されます。

[C] 登録処理に掛かる時間を利用する

③登録のControllerの処理にかなり時間が掛かる場合に可能な攻撃です。攻撃者は正常な①②③のリクエストを順次サーバに送り、③のControllerを実行中の状態にします。その間に、攻撃者は他のリクエストを送ってレスポンスから正常なトークンを取得し、そのトークンを付けて②の汚染する役のリクエストを送ります。汚染する役の②のPopulate処理が、③のクリティカルセクションに間に合えば攻撃成功です。この攻撃はクリティカルセクション以前の処理に時間が掛かる場合に成功する可能性があります。

上のA,B,Cを踏まえると以下の結論になります。

Struts1 ワンタイムトークンはレースコンディションの防御策にはならない(Bのため)
Struts2 Cの時間が掛かる処理が存在せず、かつA,Bを考慮した設定がされている時は防御策になる

時間が掛かる処理が存在しないならば、Struts2では以下の2つの設定をしてやればよいということになります。

設定1 セッションに値を保存しうるリクエスト全てについてトークンチェックする(A)
設定2 トークンチェックをPopulateよりも前に実行する(B)

しかし時間が掛かる処理が存在すれば上の設定で防御することはできません。実際にCの攻撃が可能なことは少ないと思いますが、念のため本文に書いた回避策を取った方がよいというのが筆者の考えです。

設定1の言い方を変えれば、トークンチェックをしないActionについてはParamsInterceptorが実行されないように設定する必要がある、ということになります。 レースコンディションの話は抜きにしても、セッションに値を保存するリクエストではトークンチェックをするのが望ましいです(関連: 安西氏によるセッション変数に対するCSRFの記事)。

補足2 セッションデータのDB保存機能による影響

コンテナやフレームワークの中には、DBにセッションデータを保存する機能を提供するものがあり、それらは何らかの形でセッションの内部処理を変更します。つまりそれらの使用はレースコンディションの再現有無に影響する可能性があるため、Spring(Spring Session v2.7.0)とTomcat(PersistentManager, v8.5.84)で調べてみました。

Spring(Spring Session)― 再現しない

筆者はJDBCとRedisを使う設定でSpring Sessionを試しましたが、いずれにおいてもレースコンディションは再現しませんでした。Spring Sessionでは、リクエストの度にDBに保存されているデータを取得し、それをデシリアライズしてセッションオブジェクトを作る方式になっており、そのためにセッションはリクエスト毎に異なるインスタンスになります。つまり脆弱性の前提である「同一インスタンスへの同時アクセス」という状況にならない訳です。なお、Spring Sessionを使うと全リクエストでDBアクセスが発生するので性能の問題が生じやすいですが、その代わりスティッキーセッションなロードバランスをする必要はありません。

Tomcat(PersistentManager)― 再現する

TomcatのPersistentManagerでは、コンテナのメモリに該当のセッションIDのデータがない場合にのみ、DBのデータを読みに行く方式になっています。メモリにデータがある場合には、PersistentManagerを使っていない時と同じ挙動になり、レースコンディションは再現してしまいます。なお、PersistentManagerはメモリ上のデータを優先することから、スティッキーセッションなロードバランスが必須です。

上記のように、問題が再現するか否かは方式次第です。一般的には、スティッキーセッションなロードバランスが不要なものでは、Spring Sessionのように都度DBのデータをデシリアライズしてセッションオブジェクトのインスタンスを作る方式になっている、つまりレースコンディション問題は再現しない、と考えられます。

バージョンや設定により異なる結果となるかもしれませんので、上の結果はあくまで参考と考えてください。

補足3 Spring Sessionによる回避策

上記のようにSpring Sessionの導入はレースコンディションの回避策になりえますが、Spring Sessionの導入に際しては考慮すべき点があります。

[A] 保存できるオブジェクト

セッションに保存できるのはSerializableなオブジェクトだけになります。デシリアライズ、シリアライズの際には、そのオブジェクトのreadObject(), writeObject()が実行されます。

[B] 保存タイミング

SaveModeの設定によっては、明示的にsession.setAttribute()しないとDB保存されません。またFlushModeの設定によっては、メインのスレッドが終了したタイミングのみでDB保存されるため、それ以降に別スレッドで発生した変更は失われます。

[C] インスタンスの同一性

例えば、セッションにAtomicIntegerのカウンタを持っており、リクエストの度に1減らしていると仮定します。このような場合、セッションのインスタンスが別になるとカウンタのインスタンスも別になり、正確にカウントすることができなくなります。また、インスタンスが別だと、アプリケーションで持っているオブジェクトと「==」で比較できなくなったり、一方の変更がもう片方に反映されなくなったりします。

[D] パフォーマンス

Spring Sessionでは、セッションにアクセスする全てのリクエストでDBへのアクセスが生じます。そのためパフォーマンスへの影響は少なからずあります。

上のカウンタの例は別のタイプのレースコンディションの話です。これについては次回の記事で取り上げるつもりです。

補足4 ロック対象にするオブジェクトについて

ここでは、synchronizedを使う回避策を採る場合に、ロックを取る対象のオブジェクト、つまり「synchronized(★) {…}」の★は何であるべきか?という点について書きます。本文中に示したのは、session(HttpSession)をロック対象とするコードでした。このコードは2つの点であまりよくないと言えます。

[A] リクエストをまたいでsessionが同じインスタンスであるとは限らない。
[B] 他の箇所で同じオブジェクトをロックしている可能性がある。

まずAについて補足します。これは例えば、セッションの属性(attribute)は同じインスタンスで、それを包むガワのsessionオブジェクトはリクエスト毎に別インスタンスになる実装、つまりリクエスト毎にガワであるsessionを作成して使い捨てるようなセッションの実装もありうる、ということを言っています。もしそうであるならば、リクエスト毎に異なるsessionをロックすることには意味がありません。

次にBについてです。Synchronizedによるロックは再入(リエントラント)可能ではありますが、コンテナやフレームワークの他の箇所(リクエスト処理以外)が同じsessionをロックしているならば、ServletFilterにロックを加えることによりデッドロックのパターンが生じるリスクはゼロとは言えません。また、リクエスト処理のどこかで同じロックをかけており、その中でwait()すると、その外側のServletFilterで掛けているロックも解除されてしまいます。

他のプログラムの実装をみると、SpringのsynchronizeOnSessionの初期のコードはsessionをロックしていましたが、セッションに保存している独自オブジェクトをロック対象にするように変更され、現在に至っています(コード 1, 2)。Struts2には、ワンタイムトークンの処理の際にセッションID文字列をintern()してロックする処理があります(コード)。

参考のため、SpringやStruts2と似た方式のものを含む実装例を筆者のGistに置いています。Gistには4つの実装例を載せましたが、筆者としてはSpringに似た方式(Gistの実装例のmode=4)がよいと思います。

セッションIDベースのロック(Gistの実装例のmode=2)では、changeSessionId()を行い、新しいセッションIDがSet-Cookieヘッダでクライアントに通知された瞬間に、微妙な状況になります。ヘッダを返した後もサーバ側の処理を長く続けて、その中でセッションスコープのBeanにアクセスする場合には、攻撃の余地が生じます。 Struts2, Springのコードのコメントを見ると、上のAを考慮してロック対象を決めていることが伺えます。

補足5 Java以外の処理系について

本文では、Java Servletのセッションのレースコンディションについて書きました。他の処理系ではどうなのか?ということに少し触れたいと思います。筆者が調べたのは、Java, ASP.NET, PHP, Django, Express(express-session), Railsのデフォルトの挙動です。

処理系 ロック インスタンス 保存先 備考
Java Servlet No 同じ メモリ
ASP.NET Yes 同じ メモリ v4.0.30319
PHP Yes 異なる ファイル v7.4.3 (Ubuntu)
Django No 異なる DB v4.1.2
Node Express No 異なる メモリ express-session v1.17.3
Rails No 異なる Cookie v7.0.4

表の「ロック」列はセッションのロックが行われるか、「インスタンス」列はセッションに保存したオブジェクトがリクエストまたぎで同じインスタンスになるかを示します。

何点か特筆すべき事項を書きます。

  • ロックがなく、かつインスタンスが同一になるのはJavaのみ。
  • ASP.NETでは、インスタンスは同一だがロックが掛かるためにJavaのような問題は生じない。ただし設定を変えてロックされないようにすると、ASP.NETでもリクエスト間での競合が発生しうる。
  • PHPでは、デフォルトのセッションハンドラ以外を使う時はロックされない可能性がある。例えば独自のセッションハンドラを使うLaravelは、デフォルトではセッションをロックしない。

補足6 Javaフレームワーク検証環境

Javaフレームワークの挙動確認を行ったバージョンは以下です。

IPA届け出時 2019年 再検証時 2022-2023年
Struts1 1.3.10 (同左)
Struts2 2.5.20 2.5.22, 6.1.1
Spring 5.1.5 5.3.24

補足7 過去のリサーチ例

Javaのセッションのレースコンディションに関連する過去のリサーチをいくつか紹介します。

Session Puzzles / Temporal Session Race Conditions by Shay Chen

Session Puzzleとは、機能Aと機能Bで同じ名前のセッション変数Xを使っている場合に、機能Aでセッション変数Xを書き込み、そのセッションで機能Bにアクセスすることで、アプリケーションの制限を回避するような攻撃。例えば、ログイン処理において、パスワードが間違っていても、永続的に(あるいは一時的に)セッション変数にユーザ名を保存するようなアプリケーションがありうる。一時的な場合は、セッション変数にユーザ名が存在する短い時間の間に、そのセッションを使ってユーザ情報を参照するページにアクセスすると、そのユーザの情報が取得できてしまう、というイメージ。多数のリクエストを送り処理を遅延させることで、セッションにその変数が存在する時間を引き延ばす、というアイディアについても書いている(この手法はレースコンディションのTOCTOUの攻略でも有効です)。研究者のShay Chenは、WAVSEPというスキャナの評価プロジェクト等で知られている。

Session Puzzling and Session Race Conditions ― Is It Really That Complicated?
Session Puzzles ― Indirect Application Attack Vectors
The Boomerang EFFECT ― Using Session Puzzling to Attack Apps from the Backend

Struts1の脆弱性(CVE-2016-1182 by Dehui Yin?)

Struts1の脆弱性。ValidatorFormを継承したBeanをセッションスコープにしている時に発生する脆弱性で、レースコンディションバグだと思われる。リクエストのチェックが終わった直後に、同じセッションIDを持つ他のリクエストからBeanのvalidatorResultsプロパティ(チェック結果を保持するプロパティ)等の値を変更してチェックを回避する、というものと思われる。下の記事によると任意コードの実行も可能とのこと。

The Analysis of Apache Struts 1 ActionServlet Validator Bypass (CVE-2016-1182)


次回記事では、今回取り上げたものとは別のタイプのセッションのレースコンディション問題について書く予定です。

プロフェッショナルサービス事業部
寺田健