12
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

SPAの場合のセッション(Cookie)認証におけるCSRF対策

Last updated at Posted at 2020-09-15

TL;DR

  • 書くこと:SPAの場合のセッション(Cookie)認証における、私なりのCSRF対策の解
  • 書かないこと:
    • JWTとの比較
    • 一般的なセキュリティの話(CSRFとは何か、など)
    • Cookieの詳しい仕組み

モチベーション

  • 自社でAPIの認証周りの話が出た時に、自分の考えを持てていなかったので歯痒かった
  • CSRFに関する外部の記事を読み漁ったけど、「何故そうなのか」を理解できなかった
  • 知ってたら自分のサービス作る時に良さそう

結論

まずは結論から述べます。
SPAでセッション認証を行う場合、以下を実施しておけばCSRFを防げます。

  1. APIのドメインが認証用Cookieのドメインと同じ場合:CookieのSameSite属性をLaxに指定
  2. APIのドメインが認証用Cookieのドメインと異なる場合:
    • CookieのSameSite属性をNoneに指定し、Secure属性を付与する
    • CSRF tokenを利用する
    • 事前にPreflight requestを投げる

通常はSameSite=NoneSecureの設定に加えてPreflight requestを投げるだけでいい筈ですが、tokenを利用するようにしておくと完璧だと思います。
理由は後述します。

前提:CSRFが何故起きるのか

端的に、以下の2つが要因。

  • Cookieは「ドメインをまたぐリクエスト」にも自動で付与されるから
  • サーバ側は基本、「どこから送られたか」を見ていないから

要はサイトAの認証情報(Cookie)を持って悪質なサイトBに行ったと仮定した場合、サイトBからサイトAへのPOSTリクエストを投げられると、サイトAはCookieが正しいからリクエストを受け付けちゃうんだぜ、というお話。

加えてCORSで弾こうにもリクエスト自体は到達してしまうので、CORSだけでは意味を成しません。
CORSで異なるドメインを弾くのはブラウザであって、APIサーバではないからです。

APIのドメインが認証用Cookieのドメインと同じ場合の対策

前述したとおり、CookieのSameSite属性をLaxに指定しすればOK。
SameSite属性は値によって以下のように挙動が変わります。

  • Strict:異なるドメインへのリクエストにCookieを付与しない
  • Lax:モダンブラウザのデフォルト値。異なるドメインへのPOST・画像リクエスト、XHR、iframe経由のリクエストにCookieを付与しない
  • None:以前のデフォルト値。異なるドメインへのリクエストであってもCookieを付与する

APIのドメインが認証用Cookieのドメインと異なる場合の対策

1. CookieのSameSite属性をNoneに指定し、Secure属性を付与する

前述したように、SameSite=Noneだと今までと変わりません。
最低限Secure属性を使ってHTTPS通信することを心がけましょう。

そもそもモダンブラウザではSameSite=Noneを設定する場合にはSecure属性も付与しないとリクエストできませんのでご注意下さい。

2. CSRF tokenを利用する

セッション情報以外にtokenを発行して認証に利用する仕組みです。
サーバ側で発行したtokenをブラウザに返し、VuexのStoreやCookieなどに仕込んでリクエスト時に利用します。
サーバ側はセッション情報とこのtokenを用いて認証する、というわけです。

この仕組みは以下のポイントによってCSRFを防ぎます。

  • 独自ヘッダーを付与して投げる形となるため、独自ヘッダーを付与できないフォームのリクエストから利用できない
  • 仮にtokenをCookieに仕込んだとしても、悪意のあるサイトからはSOP(Same Origin Policy)の制限によってJSからCookieにアクセスできない

3. 事前にPreflight requestを投げる

Preflight requestはリクエスト前にOPTIONSメソッドを用いたHTTPリクエストを送り、リクエストが安全かを事前に確かめるブラウザの仕組みです。
この仕組みが発生しないリクエストを「単純リクエスト」と呼びます。

以下の全てを満たすと「単純リクエスト」になります。

  1. HTTPメソッドが HEAD、GET、POSTのいずれか
  2. ユーザーエージェントによって自動的に設定されたヘッダーしか含まない
  3. Content-Typeが以下のいずれか
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

逆に上記以外の条件だとPreflight requestが発生します。
この時レスポンスヘッダのオリジンとリクエスト送信元のオリジンが一致しないとPreflight requestが失敗し、後続する実際のリクエストが送信されません。
XSSと違ってCSRFは異なるドメインから送信されるため、ここで攻撃が失敗します。

ただし以下の点で注意が必要です。

  • Preflight requestはあくまでブラウザの仕組みのため、ブラウザを介さず送信されると機能しない
  • うっかり単純リクエストを受け付けられるようにしていると抜けられる
    • 例) Content-typeapplication/x-www-form-urlencodedなどを許可してしまっている
      • → 異なるドメインからでも通常のPOSTリクエストは受け付けてしまいます…

Preflight requestだけに依存するとリスクがありますよ、というお話。

まとめ

とりあえずSPA × セッション(Cookie)認証を利用するなら以下をやっておきましょう。

  1. APIのドメインが認証用Cookieのドメインと同じ場合:CookieのSameSite属性をLaxに指定
  2. APIのドメインが認証用Cookieのドメインと異なる場合:
    • CookieのSameSite属性をNoneに指定し、Secure属性を付与する
    • CSRF tokenを利用する
    • 事前にPreflight requestを投げる

これでSPAでもCSRFから守られるね!やったね!
まぁ何をしてもXSSとかくらったらCookie送信されて終わるので頑張っていきましょう。

※ 内容に相違などあれば是非コメントください!修正します!

補足

  • Referrer属性見ればいいじゃん!と思うかもしれませんが、HTTPSで通信するとReferrerが見えませんので使えません
  • それならOrigin属性を見たらいいじゃん?!と思うかもしれません。それはアリなんですが、ホワイトリスト方式でオリジンを許可するようにしてしまうとローカルで検証しにくくなるなぁ…と思って省きました
  • HttpOnly属性を設定できれば安心と思いきや、これを設定して返してしまうと認証されたいサーバでセッション情報にアクセスできなくなってしまうので無理です

参考資料

12
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?