☠️

XSS、CSRF、CORS、Same-orign policy、cookieとかの整理

2023/01/15に公開

最近ようやく自分の中で整理がついたので備忘録として。この記事を見るよりかは、徳丸さんの本を読んだほうがより正確だと思います。間違いあるかもです。ご注意。

これらを学ぶおすすめの順番

  1. XSS
  2. cookieの振る舞い
  3. Same-origin policy
  4. CORSとプリフライトリクエスト
  5. CSRFとcookieのSameSiteフラグ
  6. おまけ cookie vs LocalStorage

ざっくり解説

  • XSS(クロスサイトスクリプティング)とCSRF(シーサーフ)は代表的な攻撃手法です。
  • Same-origin policyは、異なるドメイン間のGETリクエスト(注意:POSTではない)を拒絶するデフォルトのブラウザの挙動のことです。CORSで穴をあける必要があります。
  • CORS(シーオーアールエス or コルス)はサーバーからブラウザに対して、異なるドメインからのHTTPリクエストを許可するアクセス権限の仕組みのことです。ただし、プリフライトリクエストが飛ばないと発動しません。プリフライトリクエストが飛ぶかどうか独特の条件があるためCORSだけでは悪意のあるリクエストを防ぐことはできません。
  • CSRFは異なるドメインからCORSの抜け道を使ってcookieが自動的に送信されることで成立します。異なるドメインからcookieが自動的に送信されない設定が、最近のブラウザでは採用されてきました。それがSameSiteフラグです。
  • cookieを使わないセッション管理としてLocalStrageを使ったアクセストークンによる認証があります。LocalStrageはJSからアクセスできるためXSS攻撃が成功したあとにアクセストークンを盗まれる可能性があります。

こうみると、CSRFは登場人物多すぎますね。だからややこしい。

Cross Site Scripting(XSS)

クロスサイトスクリプティングと読みます。これが一番わかり易いです。

まず覚えること

「ユーザーから受け取った文字列をそのまま画面に表示するサイト」をねらった攻撃

例:

  • チャットサービス
  • 入力の確認画面
  • SNS

対応策

画面にデータを出力する際は、常にHTMLをサニタイジング(エスケープ処理)をするようにしましょう。テンプレートエンジンやSPAライブラリによっては、デフォルトでやってくれることもあります。

くわしくはこちらのIPAページをチェック
https://www.ipa.go.jp/security/vuln/websecurity-HTML-1_5.html

具体的に

ユーザーから受け取った文字列をそのまま画面に表示するサイトは、 意外とあります。

例:

  • チャットサービス
  • 入力の確認画面
  • SNS

一番わかりやすいのはチャットサービスやSNSです。もし、ユーザーから受け取った文字列をHTMLエスケープ処理をせずにそのまま出力してしまった場合、ユーザーは任意のHTMLタグを第3者のブラウザ上で表示することができるようになります。これは新たな攻撃の手段を提供したり、scriptタグを埋め込んでjsを実行する(つまりなんでもできる)手段を提供したりする脆弱性を持ちます。

XSSのイメージ図

変化球的なXSSの例としては、クエリから受け取った文字列をHTMLエスケープ処理をせずにそのまま出力する場合が考えられます。こちらもクエリ文字列をHTMLエスケープ処理をせずにそのまま出力してしまった場合に、同様の脆弱性を持ちます。

クエリを使ったXSSのイメージ図

cookieの振る舞い

次に代表的な攻撃のCSRFに移りたいところですが、XSSよりも前提知識が多いため、先にそちらを消化します。前提知識としてはcookieの挙動やCORSやSame-origin policyがあげられます。

まずは、クッキーです。主にセッション管理に使います。ブラウザに標準搭載されています。セッションを保持したいときには、よくJWTとcookieの2つが選択肢にあがります。

まず覚えること

  • サーバー側がブラウザにHTTPレスポンスヘッダを通してcookieをセットし、次回以降のリクエストでcookieを要求する
  • cookieはブラウザにドメイン単位で保存される
  • そのドメインにHTTPリクエストが送信される際に、他のドメインのサイトからのリクエストであっても、cookieが自動的に送信される。例外あり(SameSiteフラグ)。

具体的に

Webサービスを構築する際に、ログイン状態を保持しておきたいことがあります。このログイン状態のことをセッションと呼んだりします。セッションを管理する方法にはいくつかありますが、HTTPリクエストの場合はリクエストヘッダかcookieを使うことが多いです。どのようにセッション管理を実現するかについては、ググってみてください。わかり易く解説してくれるサイトがたくさんあります。

さて問題は、このcookieを使ってセッション管理しているサイトに対しては、2種類の攻撃をすることができることです。

  1. 適当にcookieにセッション変数を入れることで、他の誰かのセッションに不法にあいのりする攻撃(セッションハイジャック攻撃)
  2. そのサイトに対してHTTPリクエストが送信される際に、他のドメインのサイトからのリクエストであっても、自動的にcookieが送信されることを悪用した攻撃(CSRF攻撃)

1つ目をこのセクションで解説します。2つ目はCSRFセクションで出てくるので、このセクションでは解説しません。

セッションハイジャック攻撃について。
これは、セッション変数が予測しやすい単純なものを使っている場合や、通信が暗号化されておらずセッション変数を盗聴しやすい場合に成功する攻撃です。以下のシーケンス図をみるとわかりやすいと思います。また、cookieの設定によってはJSからcookieを取得できるようにできます。この場合、XSS攻撃が成功するとセッション変数を盗まれる可能性があります。

セッションハイジャック攻撃の例

くわしくはこちらのIPAページをチェック
https://www.ipa.go.jp/security/vuln/websecurity-HTML-1_4.html

Same-origin poilcy

まず覚えること

  • 異なるドメインに対してのGETリクエストを禁止するブラウザ側の挙動
  • サーバー側のCORSの設定でこの挙動を上書きすることができる
  • POSTリクエストには効果がない
    • レスポンスは失敗するがリクエストは成功してしまう
    • プリフライトが飛ぶならCORSが適用されるため防ぐことはできる
      • プリフライトが飛ぶかどうかはリクエストの条件次第

具体的に

より正確には、異なるドメインではなく、プロトコル(httpかhttpsか)とホスト(example.com)とポート(example.com:3000)のいずれかが異なるときに、適用されるポリシーです。最近ではフロントエンドとバックエンドが別れており、それぞれ異なるURLになることが多いです。例えば、フロントエンドはhttps://example.comでバックエンドはhttps://api.example.comなどです。この場合サブドメインなのでホストが異なります。そのため開発時に両方ともlocalhostで開発サーバーをたてて通信していた場合、実際に異なるドメインにデプロイするとSame-origin policyで通信がうまくできなくなります。バックエンド側でCORSの設定が必要です。

とくにドキュメントのこの部分を読んでください。

https://developer.mozilla.org/ja/docs/Web/Security/Same-origin_policy#異なるオリジンへのネットワークアクセス

  • 異なるオリジンへの書き込みは、概して許可されます。例えばリンクやリダイレクト、フォームの送信などがあります。まれに使用される HTTP リクエストの際はプリフライトが必要です。
  • 異なるオリジンの埋め込みは、概して許可されます。例は後述します。
  • 異なるオリジンからの読み込みは一般に許可されませんが、埋め込みによって読み取り権限がしばしば漏れてしまいます。例えば埋め込み画像の幅や高さ、埋め込みスクリプトの動作内容、あるいは埋め込みリソースでアクセス可能なものを読み取ることができます。

埋め込み系、特にiframeとかややこしいので、僕もまだ理解できてないです(宿題)。
ここで、1番やばいのは、1番上のこれです。

異なるオリジンへの書き込みは、概して許可されます。例えばリンクやリダイレクト、フォームの送信などがあります。まれに使用される HTTP リクエストの際はプリフライトが必要です。

つまり、一般的なformを通したリクエストはプリフライトリクエストも飛ばないのでSame-origin policyが適用されないのです。

悪意のある外部ドメインのhtmlがSame-originの抜け穴を使ってPOSTリクエストをする例
<form action="https://example.com/send-money?to=hogehoge">
  <input value="あああ" name="userId" hidden></input>
  <button type="submit">大金を稼ぐ方法はこちら</button>
</form>

CORSやプリフライトリクエストの話をしたほうがよさそうですね。しかし、なぜこのような穴のある仕様を...
また、上記の例、実はCSRF攻撃でもあります。「うちはログインしないとAPI叩けないようにしているよ!」という開発者の方、実はセッション管理にcookieを仕様していると、cookieの仕様的によくないことが起こりましてね... これはCSRFセクションに譲ります。

CORSとプリフライトリクエスト

まず覚えること

  • CORSはSame-origin policyの挙動を上書きして許可する(ホワイトリスト形式)
  • サーバー側で設定する
  • レスポンスヘッダにAccess-Control-Allow-Originで許可ドメインを指定する
  • 異なるドメイン間のリクエストの場合、ブラウザはまずCORSプリフライトリクエストをHTTPメソッドOPTIONで送信して、CORSチェックをし、許可されていれば実際のリクエストを飛ばす。
  • ただし、CORSプリフライトレクエストは単純なリクエストの場合は飛ばない。CORSチェックをスキップできる。
    • 単純なリクエストは基本的にapplication/jsonでやり取りしているAPIリクエストでは発生しないが、formを使ったリクエストの場合だと発生する。
    • 単純なリクエストはブラウザの古い仕様formとの互換性のため残ってる

具体的に

CORSは異なるドメイン間でのHTTPリクエストを許可するための仕様です。デフォルトではSame-origin policyによってブロックされます。つまりCORSはブラックリスト形式ではなくホワイトリスト形式です。レスポンスヘッダにAccess-Control-Allow-Originと許可するドメインを指定することで、設定できます。

どのように許可しているのでしょうか?プリフライトリクエストという特殊なHTTPリクエストを使います。このリクエストは、ブラウザが自身とは異なるドメインにHTTPリクエストを送信する際に、前もって自動的に送信されます。HTTPメソッドはOPTIONSです。このプリフライトリクエストの結果、サーバーがこのドメインからのリクエストが許可しているかどうかを知ることができます。許可されていれば、ブラウザは本番のリクエストを送信します。

ただし、抜け穴があります。単純なリクエストだとCORSプリフライトリクエストが飛ばないのです。

単純なリクエストになる条件は結構複雑です。詳しくは、以下のリンクを見てください。ここで覚えておくのは、formを使ったリクエストは単純なリクエストになりやすく、CORSチェックを回避してしまうということです。
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#単純リクエスト

なんかもう、わけわかんなくなってきましたね笑。

CSRF

ここまでで、CSRFを解説するための、前提知識が揃いました。登場人物が多すぎるorz。

まず覚えること

  • CSRFはブラウザの挙動を利用しつつ、セキュリティの抜け道を巧みに使った攻撃
  • Same-origin policyやCORSのぬけ穴を利用
  • 特にcookieは、異なるドメインからのリクエストであっても、自動的に送信されるため、これを悪用
    • SameSiteフラグでこの挙動を制御できる。最近の最新のブラウザであれば、デフォルトで有効になっているはず。

対応策

  • cookieを使ったセッションを利用するなら、formにはCSRFトークンをつけ、cookieのSameSiteフラグは有効にしておく
  • 使わないContent-Typeは弾く
  • リスクの高い操作の前にはパスワード入力を求める

くわしくはこちらのIPAページをチェック
https://www.ipa.go.jp/security/vuln/websecurity-HTML-1_6.html

具体的に

前提として、bank.comという架空の銀行サイトにAさんがログインしていたとします。

Aさんが、悪意のあるサイトを開くと、「大金を稼ぐ方法はこちら」というリンク(に似せたフォーム送信ボタン)があり、Aさんはそれをクリックしてしまいました。

bank.comはCSRF対策をとってなかったので、悪意のあるリクエストが実行され、Aさんから悪い団体にお金が送金されてしまいました。

悪意のある外部ドメインのhtmlがSame-originの抜け穴を使ってPOSTリクエストをする例
<form action="https://bank.com/send-money?to=hogehoge">
  <input value="あああ" name="userId" hidden></input>
  <button type="submit">大金を稼ぐ方法はこちら</button>
</form>

CSRFのシーケンス図

なぜこのようなことが起きてしまうのでしょうか?原因は2つです。

  • Same-origin policyとCORSがformに対しては抜け穴がある
  • cookieが異なるドメインからでも自動送信されてしまう

これらの原因を防ぐために、対応策として根本的に有効なのは

  • cookieを使う場合は、CSRFトークンで対策する
  • cookieが異なるドメインから自動送信されないようにSameSiteフラグを有効にしておく

です。

cookieの強み

  • JSからアクセスできないように設定できるので、XSSで侵入されたときにセッション変数を盗まれない
  • 追加コードなしで、有効期限を設定できる
    cookieの弱み
  • 異なるドメインから自動送信されてしまう。ただし最近は、ブラウザベンダがSameSiteフラグが設定されていなければデフォルトで有効にする方向へ方針を変えていっている。具体的には各ブラウザの対応を調査したほうがよさそう。

LocalStorageの強み

  • JSとの相性が良い
    LocalStorageの弱み
  • おなじドメインであればJSからアクセスできる。XSSで侵入されたときにアクセストークンを盗まれる
  • 有効期限を設定できない。アクセストークン自体に期限を保持しておき、リクエスト時にコードで期限切れを判別する必要あり。

またアクセストークンをLocalStorageではなくメモリ上で管理する方法もあるみたいです。
https://blog.flatt.tech/entry/auth0_access_token

ここらへんは徳丸さんのスライドが詳しいと思います。
https://www.slideshare.net/ockeghem/phpconf2021spasecurity

参考

https://note.crohaco.net/2019/http-cors-preflight/

Discussion