CSRF 対策用トークンの値にセッション ID そのものを使ってもいい時代は終わりつつある

CSRF 脆弱性対策には攻撃者の知り得ない秘密情報をリクエストに対して要求すればよく、そのような用途としてはセッション ID がお手軽でいいよねという時代があったかと思います。

いや、もちろん、 CSRF 対策の文脈だけで言えば今も昔も間違いというわけではありません。セッション ID が秘密情報であるのは Web アプリケーションにおいて当然の前提ですので、 CSRF 対策としてリクエストに求めるべきパラメータとしての条件はたしかに満たしています。

たとえば 『安全なウェブサイトの作り方』 改訂第6版では以下のように解説されています。

6-(i)-a. (中略) その「hidden パラメータ」に秘密情報が挿入されるよう、前のページを自動生成して、実行ページではその値が正しい場合のみ処理を実行する。

(中略) この秘密情報は、セッション管理に使用しているセッション ID を用いる方法の他、セッション ID とは別のもうひとつの ID(第 2 セッション ID)をログイン時に生成して用いる方法等が考えられます。

『安全なウェブサイトの作り方』 改訂第6版 p. 30 より引用

ですが、いくら CSRF 対策になるとはいっても、 CSRF 対策用トークンとしてセッション ID そのものを使うのはもうやめた方がいいですよ、というのを書いていきます。

なぜセッション ID を使ってはいけないか

理由として思いついたものをふたつ挙げます。

  • セッション cookie に対する HttpOnly 属性の利用が一般的になってきた [1]
  • SSL 通信に対する BREACH attack という攻撃手法が発表された

これによって、「セッション cookie は盗まれないが CSRF 対策用トークンは盗まれうる」という状況が生まれ、「CSRF 対策用トークンがセッション ID 自体であったためにセッションハイジャック攻撃に繋げられてしまう」可能性が出てくることになります。

具体的にどういうことなのかについて少し解説していこうかと思いますが、ここまでで、「あー」と思った人は自分のサイトとかもろもろを再点検すればいいだけの話で、続きを読む必要はないです。以上です。お疲れ様でした。

BREACH attack の影響を受ける場合

実はこの記事のドラフト完成直前に思いついたのですが、 BREACH attack の影響を受ける場合も CSRF 対策用トークンとしてセッション ID を使わない方がよさそうです。

Web サイトが HTTP と HTTPS の両方でサービスを提供している場合、通信路上の攻撃者が HTTP 通信時 (平文通信時) の通信内容を盗聴して得たセッション cookie を悪用して、 HTTPS で提供されるリソースに対してセッションハイジャックされる可能性があります。サービスの性質によってはこの種の攻撃に対するリスクを受容できないとして、 HTTP 通信と HTTPS 通信時に用いるセッション cookie を分け、 HTTPS 通信時の cookie に対しては secure 属性を指定することで対策をしているのではないかと思います。

これによって、正当な利用者が HTTPS 通信上でサービスを受けている場合に必要なセッション cookie を盗聴することはできなくなりますが、 CSRF 対策用トークンと HTTPS 通信用のセッション ID が共通である場合、 BREACH attack によってセッション ID を推測されてしまうかもしれません。

BREACH attack は、 HTTPS 通信によって暗号化されたレスポンスボディ (HTTP 圧縮されたもの) に対する、サイドチャンネル攻撃の一種です。詳細は http://breachattack.com/ などを参照していただきたいのですが、以下簡単に説明します。

HTTPS 通信専用のセッション ID が CSRF 対策用トークンとしてレスポンスボディに含まれるページに対して、 body=csrf_token+value%3D0 といった POST パラメータを指定したリクエストを攻撃者が利用者に強制した結果、レスポンスが以下のような内容になるとします。

<p class="error">csrf_token: 必須項目です。</p>
<form method=post action="/">
    <input type=hidden name=csrf_token value=123456789abcdef>
    <textarea name=body>csrf_token value=0</textarea>
    <input type=submit>
</form>

リクエストの一部をそのままレスポンスの一部として返していることがわかりますね (ちょっとわざとらしい感じがありますが)。

さて、この HTML 断片を含む暗号化された HTTPS レスポンスを攻撃者が盗聴した結果、その長さは 1024 バイトでした。

ここで body+value%3D0 の末尾の 01 に変えてリクエストを強制させると、今度のレスポンスは <textarea name=body>csrf_token value=1</textarea> を含むことになりますが、 csrf_token value=1 は「hidden パラメータ」の一部として既に登場しているため、 HTTP 圧縮によって暗号化された HTTPS レスポンスの長さが 1024 バイトよりも小さくなります。——というのは極めて単純化した話で、実際にはそう簡単にはいかないようですが (実際に検証しようとしましたが、前提となる知識が足りなすぎて力尽きました……)、このように、レスポンスに含まれる秘密情報と同じ内容を繰り返し登場させた場合とそうでない場合で HTTP 圧縮したレスポンスの長さが変化することを利用して、平文を得ることなくリクエストの一部を推測することができるという攻撃です。

これによってセッションベースの CSRF 対策用トークンは破られうるよね、ということで DjangoRails なんかでは対策が検討されていたりするようです。

で、つまるところ、 CSRF 対策用トークンがセッション ID そのものである場合、この攻撃によって CSRF 対策用トークン (= セッション ID) が盗まれることになるため、セッション cookie を secure 属性付きで発行している意味がなくなります。もっとも、この場合はまず BREACH への対策をするべきではないかとも思いますが、 CSRF 対策用トークンの盗聴程度であればまあ許容できるけれども、それがセッションハイジャック攻撃に繋がるのであれば看過できない、という向きもあることでしょう (……我ながら無理があるな)。

ただ、少なくとも海老原レベルの人間にはまだ有効な exploit code を自前で作れるに至っていない (論文をちゃんと理解できていなくて、 sniff したレスポンスの長さが期待通りに変化しないという問題にぶち当たってから抜け出せていない [4] ) のと、 HTML エスケープによって推測に必要な文字列がそのままレスポンスに出力されるのを阻まれることが多そうで、現実にこの攻撃による被害が出てくるのはまだまだ先になるかもしれません。

CSRF 対策用トークンはどうしていけばいいか

まあ、どうすればいいかというと、

6-(i)-a. (中略) その「hidden パラメータ」に秘密情報が挿入されるよう、前のページを自動生成して、実行ページではその値が正しい場合のみ処理を実行する。

(中略) この秘密情報は、セッション管理に使用しているセッション ID を用いる方法の他、セッション ID とは別のもうひとつの ID(第 2 セッション ID)をログイン時に生成して用いる方法等が考えられます。

『安全なウェブサイトの作り方』 改訂第6版 p. 30 より引用

「セッション ID とは別のもうひとつの ID(第 2 セッション ID)をログイン時に生成して用いる方法等」を採用すればいいわけですが、セッション ID とまったく独立した形で生成するというよりは、単にセッション ID を SHA-2 ファミリのハッシュ関数あたりを通してそれを使えばいいかと思います。鍵とか salt とか付きでハッシュ値を得る必要は、少なくともこのエントリの文脈で言えばまあないでしょう。

これによって、前述した BREACH attack を受けた場合も盗まれるのはセッション ID そのものではなくなるため、影響は CSRF どまりで済みます。 BREACH attack そのものへの対策はこのエントリのスコープ外なので、研究者自身により公表されている情報や JVN で掲載されている情報 を参照してください。まあ HTTP 圧縮を無効にするのが一番簡単ですが、それが難しい場合でもお使いのライブラリやフレームワーク側の対策を待って、独自実装には走らないようにするのが無難かなとは思います。

[1]え、あれ、一般的ですよね?
[2]ブラウザが対応していれば。とはいえ、 ほとんどのブラウザは対応済み です。
[3]document.cookie にこの種の情報が格納されることを期待した機能 (ブラウザ拡張等も含まれるかもしれない) は動かなくなるくらいです。よっぽど変なブラウザを使っていない限り、 HttpOnly に未対応でも単に無視されるだけです。
[4]この土日結構頑張ったんですけどね…って、あああ、ブロック長とかまるで考慮してなかったせいじゃねひょっとして……
comments powered by Disqus

Recently entries