Closed14

CSRF 対策はいまだに Token が必須なのか?

JxckJxck

CSRF 対策は One Time Token を form なりに付与して、サーバ側でチェックすれば良い。
それをデフォルトでサポートしてるフレームワークなどもあるし、なくてもライブラリでいくらでも対応できる。
どうせ完全にステートレスなサービスはなかなかないので、サーバ側に redis や memcache を用意するのも別に大変じゃない。

なので、 CSRF 対策として Token を付与するのは、最も安全で推奨できる方式ではある。

っていうのを踏まえた上で、もう SameSite=Lax デフォルトだけど、今でも Token 必須なの?みたいなのがたびたび話に出るので、いい加減まとめる。

JxckJxck

前提

この話は、スコープがどこなのかによって話が多少変わるので、そこを絞る。
今回は Passive ではなく Active に対策していく場合を考えるので、前提をこうする。

  • SameSite=lax or strict は自分でつける
    • lax by default に頼らない
    • SameSite 自体を実装してれば OK
  • 古いブラウザは考えない
    • chrome,safari,firefox,edge のみ
    • stable のみ
    • IE や古いバージョンのサポート考えるなら token 入れるで終わり
  • GET で副作用のある API を考えない
    • その設計の問題
  • XSS があったらを考えない
    • あったら何しても無駄

逆を言えば、こういう条件で作られたアプリで「CSRF token がないけど攻撃耐性のあるサービスは実装できるか?」みたいなイメージで考える。

JxckJxck

仕様は CookieBis (Cookie の更新ドラフト)で策定中。
IETF で、そろそろ WGLC かなって話をしてたと思うので、今年には RFC になるか?(そろそろ出さないと次は CHIPS とかも待ってるし)

とにかく、ドラフトだけど生煮えな記述じゃないと思って良いはず。

https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-13#document-requests

Lax enforcement provides reasonable defense in depth against CSRF attacks that rely on unsafe HTTP methods (like POST), but does not offer a robust defense against CSRF as a general category of attack:

Lax は unsafe な HTTP メソッド(e.g. POST)に依存する CSRF 攻撃に対して妥当な多層防御を提供するが、一般的な攻撃のカテゴリとしての CSRF に対する強固な防御は提供しない。

で理由が二つ。

  1. Attackers can still pop up new windows or trigger top-level navigations in order to create a "same-site" request (as described in Section 5.2.1), which is only a speedbump along the road to exploitation.

popup で window を開いたり top level navigation で same-site なリクエストはできるので、攻撃への多少の障害にしかならない

  1. Features like <link rel='prerender'> [prerendering] can be exploited to create "same-site" requests without the risk of user detection.

prerender すればユーザにバレずに same-site リクエストができる。

これについて考えてみる。

JxckJxck

つまり、ここだけをみると「SameSite なリクエストを送る方法は他にもあるから」という理由だと読める。

確かにできるが、少なくとも Navigation と Prerender で発生するのは Safe Method でのリクエストなはず。これは、前提に書いたようにサーバ側が Safe Method で副作用のある API を持ってなければ問題ないはず。

window.open でサイトは開けるが、開くこと自体はやっぱり GET だ。開いただけで発生するリクエストで問題があるなら、それはもう window.open じゃなくても攻撃はし放題だろう。

window.open で開いたページを操作して、例えば form を submit するなどさせて、成立する攻撃を CSRF と言って良いのか(そもそもそれは Cross Site じゃないし)は微妙だ、どっちかというと Click Jacking に近いと思う。

が、まあこれに完全に対応するなら、まあ window.open で開かせないということになるだろう。
CSP で navigate-to があったら防げたのかもしれないけど、今はもう亡くなってる。なんか他に防ぐ方法あったっけ?(TODO)

https://github.com/w3c/webappsec-csp/pull/564
https://bugs.chromium.org/p/chromium/issues/detail?id=1456742
https://chromestatus.com/feature/6457580339593216

JxckJxck

一旦おいといて OWASP には以前から CSRF CheetSheet があるので確認する。

https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html

実は SameSite が入ったことで、この Cheatsheet 更新できるんじゃない?という話は一度 issue を建てたことがある。

https://github.com/OWASP/CheatSheetSeries/issues/940

これに対しては、それじゃだめで、その理由も書いてある。ってことだった(その理由の更新の話をしてるんだけど、まあ変わってないということだったと捉えている)

ところが、最近 (2023/12/4) になって、このページががらっと更新された。

https://github.com/OWASP/CheatSheetSeries/pull/1247

Editorial と言ってて、 related issue とか discussion がない。
が、なんか色々変わってるように見える。 issue で示されたリンクも死んでる。

これをもう一度精査して、 OWASP の主張自体が変わってるようなら、「どういう議論で変わったのか?」は聞いてみる必要がある。

JxckJxck

先に言っておくのを忘れたけど、 CSRF に対する対策として考えられるのは以下のようなもの。

  1. Session Cookie には SameSite=Lax を付与する
  2. Unsafe リクエストは Origin と Host が同じドメインかをチェックする
    • もちろん example.com.attack.com みたいな部分一致のバイパスはしないよう実装する前提
    • 今は form submit に Origin ヘッダがついてる(数年前に Firefox も実装したので揃った)ことを踏まえての対策
    • 基本ケースでは、送られない可能性のある referrer に頼らないでチェックできるはず
  3. Sec-Fetch-* 系のチェック
    • 全てのブラウザが送るわけじゃないので、「送ってきてたらチェックする」という多層防御
    • ミドルウェアなどで対応しやすい

と加えて

  1. __Host- prefix をつけるとなお良い
  2. Cookie を read/write で分離して Lax と Strict にできるとなお良い

2, 3 はつまり、フリーハンドで書くとイメージこんな感じ

app.use((req, res, next) => {
  // origin と host の一致
  const headers = req.headers
  cosnt {host, origin} = headers 
  if (host !== (new URL(origin)).host) return res.send(400)

  // あったら意図したやつか
  if (headers.secFetchDest && headers.secFetchDest !== "document") return res.send(400)
  if (headers.secFetchMode && headers.secFetchMode !== "navigate") return res.send(400)
  if (headers.secFetchSite && headers.secFetchSite !== "same-origin") return res.send(400)

  return next()
})

新規開発なら最初からこういうの入れておいて、逸脱ケースを把握しながら進められるとベスト。

ただ、これは form submit な CSRF が前提なので、 fetch() とかだと話(値)は変わる。

JxckJxck

今回考えているスコープで OWASP を再確認。特に SameSite について言及しているところは

SameSite Cookie Attribute can be used for session cookies but be careful to NOT set a cookie specifically for a domain. This action introduces a security vulnerability because all subdomains of that domain will share the cookie, and this is particularly an issue if a subdomain has a CNAME to domains not in your control.
SameSite Cookie 属性はセッション Cookie に使用できるが、 Domain のついた Cookie に設定しないように注意が必要。このアクションでは、すべてのサブドメインが Cookie を共有するため、セキュリティ上の脆弱性が生じます。これは、サブドメインが管理下にないドメインに対する CNAME を持っている場合に特に問題になる。

これは Domain 属性は __Host prefix があればそもそも付与できない。またサブドメインが CNAME Cloaking されてるケースは、なんというかそういうことする奴が悪いで良さそう。

本題は Defense In Depth で紹介されている SameSite

It is important to note that this attribute should be implemented as an additional layer defense in depth concept. This attribute protects the user through the browsers supporting it, and it contains as well 2 ways to bypass it as mentioned in the following section. This attribute should not replace a CSRF Token. Instead, it should co-exist with that tokento protect the user in a more robust way.
この属性は、追加の層の多層防御の概念として実装する必要があることに注意することが重要だ。この属性は、サポートするブラウザを通じてユーザーを保護する。また、次のセクションで説明するように、これをバイパスする 2 つの方法も含まれている。この属性は CSRF Token を置き換えるべきではない。代わりに、より堅牢な方法でユーザーを保護するために、 Token と共存する必要があります。

つまり OWASP 的にはあくまで「多層防御」の一個(Token の次点)という扱いは変わらず。
そして、ここに書かれている 「2 つのバイパス方法」は、さっき Cookiebis の draft で紹介したものなので、同じ話。

ということは、さっきの 2 つがクリアされてれいれば、他のリスクは提示されてない?

JxckJxck

OWASP の中では、Origin と Host ヘッダの比較についても、 Defence in Depth として役立つと書いてる。が、注意書き付き。

  • Origin ヘッダについて
    • 存在したらチェックする
    • なかったら referer でもいい
    • チェックは厳格に (example.org.attacher.com で迂回できないよう)
    • どっちもなければ request 自体を deny するのがおすすめ
    • 含まれないことはほぼないからチェックしよう
  • 注意点
    • Proxy の後ろにあると Origin ヘッダの値変わったりするから注意
      • その場合、ソースに許可するホストを直書きするか、 X-Forwarded-Host とかでもらう
    • 302 挟んだらなくなったり、Origin が null だったりする場合もあるから注意
    • Referer も省略される場合があるから注意
    • そういう注意が必要なのが 1~2% あって、それを取りこぼさないために Token が主であくまで多層防御がおすすめだよ

正直、注意点については割とおせっかいな部分があるように思える。
そもそも Referer が信頼の根拠にならかったのは、プライバシー設定で落とせるようになっていたから。だから代わりに Form で Origin が送られるようになった ということを考えると Origin を null にしているユーザが仮にいたとして(どういう環境かは知らんが)、そうした環境は「セキュリティ上適切にサポートできない」という意思決定はおかしくないはず。
(むしろ、それを積極的にサポートする方が、ユーザのためになってない可能性がある)

ここではなんの根拠もなしに 1 ~ 2 % いると言っているが、それが自分が作るサービスでもいるのかは別途測らないとわからないし、いたとしてそれをサポートするかは、個々のポリシー次第だろ、と思う。

JxckJxck

一旦 <form> submit による典型的な CSRF だけに絞ると、 OWASP で言及されているのはそのくらい。
他は Ajax 系での話で、そっちはまた対応方法がいくらでもあるので、一旦おいておく。

あと、OWASP はあらゆる場面を想定して書かれているため、 Token を入れれば大丈夫だという主張はもっともだけど、最初の条件を満たしたまとも実装で考えると、 SameSite だけでのリスクは見えてくる。

つまり、

  • Lax, Host prefix がついた Session Cookie になってる
  • safe method での副作用がない
  • Host / Origin チェックと、 Sec-* 系を見ている
  • redirect で post されてくるケースはない
  • origin が null なリクエストは、特殊な環境で危険と考えて deny する
  • proxy などで host/origin が伝わらないみたいな初歩的なのは、全部実装中に解決してる

みたいなところが防がれていれば、「それでもリスクだ」というケースは、少なくとも文面からは見えてこない。

だから安全とは言いきれないが、その論法だと「安全である」ことはいつまでも証明できないので、まあ Token を使えという話になる。

JxckJxck

そもそも Token を入れるコストは、 Rails のような統合されたものを使ったり、メジャーなフレームワークの middleware などで低コストで入れられるので、実装コストが十分に下がっている場合が多い。

だから、別に入れれば良い。

一方、そのためにストレージが必要で、そこにコストもかかるし、 I/O でパフォーマンスにも多少影響は出ているはず。
なくせるなら無くした方がシンプルなのも事実なので、「絶対に必要だ」でないのなら、「こうすることでなくせる」は考えてみる価値がありそうというのが、今回の調査。

JxckJxck

TODO: あとは、 SameSite でもバイパスできた事例を少し探してみる。

JxckJxck

そういえば、なぜ今更この話かというと、 Server Actions が CSRF Token 入れないって話を書いていたのを見ていたから。

https://nextjs.org/blog/security-nextjs-server-components-actions#csrf

Next の言い分はこう。

All Server Actions can be invoked by plain <form>, which could open them up to CSRF attacks. Behind the scenes, Server Actions are always implemented using POST and only this HTTP method is allowed to invoke them. This alone prevents most CSRF vulnerabilities in modern browsers, particularly due to Same-Site cookies being the default.
すべての Server Action は、プレーンな <form> で呼び出すことができる。裏では、 Server Action は常に POST を使って実装され、この HTTP メソッドだけが呼び出すことを許されている。これだけで、特に Same-Site Cookie がデフォルトであるため、最近のブラウザではほとんどの CSRF 脆弱性を防ぐことができる。

Same-Site Cookie の "Lax が" デフォルトなんだけどまあそれはいい。あと、デフォルトかどうかじゃなくて明示的に付与すれば良い。

As an additional protection Server Actions in Next.js 14 also compares the Origin header to the Host header (or X-Forwarded-Host). If they don't match, the Action will be rejected. In other words, Server Actions can only be invoked on the same host as the page that hosts it. Very old unsupported and outdated browsers that don't support the Origin header could be at risk.
さらに、Next.js 14 の Server Action は、Origin と Host (か X-Forwarded-Host)を比較する。一致しない場合、 Action は拒否される。言い換えると Server Action は、それをホストするページと同じホスト上でしか実行できない。Origin ヘッダをサポートしていない、非常に古いブラウザは危険にさらされる可能性がある。

Host と Origin のチェックは追加でやるらしい。それがうまくいかないと弾いている。

Server Actions doesn't use CSRF tokens, therefore HTML sanitization is crucial.
Server Actions は CSRF Token を使用しないため、HTML のサニタイズは非常に重要です。

??? CSRF Token を使うかどうかと HTML のサニタイズは関係ないと思うが。
「XSS があったらダメになる」みたいなことを言いたいのかもしれないが、 XSS があって任意のスクリプトを実行できるなら、大抵は何をしてもだめなのでわざわざ書かないで良い。

When Custom Route Handlers (route.tsx) are used instead, extra auditing can be necessary since CSRF protection has to be done manually there. The traditional rules apply there.
カスタムルートハンドラ (route.tsx) を代わりに使用する場合、CSRF 保護を手動で行う必要があるため、特別な保護が必要になる。そこでは従来のルールが適用される。

レール(CoC)を外れるなら自分で Token を付与するなりしろよということか。それはまあそう。

説明にはちょっと怪しいところもあるが、まあ Serve Action がこのまま流行って、それでも CSRF が報告されなければ、この方式でも対応できる根拠にはなるかもしれない。

このスクラップは3ヶ月前にクローズされました