0
0

More than 1 year has passed since last update.

JWTのFE <=> BE (API)間でのセキュアな保持方法について調べてみた。

Last updated at Posted at 2021-10-02

きっかけとなった疑問

そもそも自分がFE側のセキュリティに詳しくないことが問題だったのだが、、 :laughing:

  • OAuth2で認証管理を行なっている場合は、CSRFはdisableにしても良いのか?

引用:

To answer your first question, in the context that you describe, you do not need CSRF protection. The background of CSRF protection is to ensure that the user is not tricked into doing some unwanted action.
For example, in pure theory, you could have logged into a bank's website (and thus established a session) and then went to some shady website. This site could have a form making a POST request to the bank's APIs. Because you have a session there, if the endpoint is not CSRF protected, then the request may go through.
As such, CSRF mostly acts as a protection against browser + session based attacks. If you expose a pure REST API with e.g. OAuth protection, then I don't see any reason for CSRF.
As you use spring boot, you could also disable CSRF using the application.properties / application.yaml configuration file.
security.enable-csrf=false
You can check out the Common Application Properties documentation page for more out-of-the-box configuration options.

質問内容としても、StatelessなAPIでTokenを用いた認証を行なっている (HMAC認証)というところで、こちらのユースケースとも類似している。なので、CSRFをDisableとした。

しかし、本当にこれで良いのだろうか?

きっかけはまた同じStackoverflow link 内にあった補足コメント。

If you are using a browser, then you need CSRF protection in cases when you store the credentials in local or session storage in the browser. Note you are not using cookies but still. So I'm not sure this is a correct answer. See: stackoverflow.com/questions/21357182/…

つまり、FE側でLocalStorageやSessionStorageでJWTを保管するような場合には、CSRF対策が必要 とある :astonished:

確かに考えてみると、、

  • JWTがXSSを用いて、LocalStorage/SessionStorageから抜き取られるようなことがあったらどうする?
  • そしたら悪意のあるユーザーが有効なJWTをそのまま丸ごと疑似リクエストとして送信できてしまう
  • そもそも、FE側でJWTを管理する、ベストで安全な方法ってなんだろう、、

という疑問が生じてきたのである。 :question:

JWTの保管方法のまとめ (及びJWTのCSRF対策についての)

参考LINK
- https://stackoverflow.com/questions/21357182/csrf-token-necessary-when-using-stateless-sessionless-authentication (回答 (63) が参考になった)
- https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage

方法(1) LocalStorage/SessionStorageを利用する方法

Authorization Serverが JWT TokenをResponse Bodyとして渡す。
例:
Response

HTTP/1.1 200 OK

  {
  "access_token": "eyJhbGciOiJIUzI1NiIsI.eyJpc3MiOiJodHRwczotcGxlL.mFrs3Zo8eaSNcxiNfvRh9dqKP4F1cB",
       "expires_in":3600
   }

FE

function tokenSuccess(err, response) {
    if(err){
        throw err;
    }
    $window.sessionStorage.accessToken = response.body.access_token;
}

その後、FEはAPIへ HTTP HeaderとしてTokenを渡す。

HTTP/1.1

GET /stars/pollux
Host: galaxies.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsI.eyJpc3MiOiJodHRwczotcGxlL.mFrs3Zo8eaSNcxiNfvRh9dqKP4F1cB

方法(2) Cookieを利用する方法

一方、Cookieを利用する場合は、Cookieはデフォルトでは同じDomain内でのみ受け渡し/読み取り可能なので、おそらく認証サーバ、FE、リソースサーバ(API)全て同じドメインであるということを前提とした例なのだろう。

認証サーバのレスポンス (access_token というCookie nameは業界スタンダードなのか?)

HTTP/1.1 200 OK

Set-Cookie: access_token=eyJhbGciOiJIUzI1NiIsI.eyJpc3MiOiJodHRwczotcGxlL.mFrs3Zo8eaSNcxiNfvRh9dqKP4F1cB; Secure; HttpOnly;

上記のCookieはそのままAPIへのリクエストへ利用される。 (以下例)

GET /stars/pollux
Host: galaxies.com

Cookie: access_token=eyJhbGciOiJIUzI1NiIsI.eyJpc3MiOiJodHRwczotcGxlL.mFrs3Zo8eaSNcxiNfvRh9dqKP4F1cB;

上記2方法での考察

LocalStorage + Http Headerの場合

  • XSSのリスクが高まる
    • いくら自分らで開発しているJSが安全であったとしても、実際にはさまざまな3rd party libraryやTracking tool (Google アナリティクスとか)を組み込んでいる場合がほとんど。全てのJS FileがXSS脆弱性がないということを証明することは非常に難しい
    • 昨今はBowerやnpmなどの様々なパッケージ管理ツールで3rc party scriptsを独自ドメインとして組み込んでいる
    • なのでブラウザではあまりお勧めされていない
    • しかし、SPAの場合は、Storageを使わずに、メモリ空間の中にJWTを保持しておけばXSSのリスクは減るかも

From what I can gather the general trend is to avoid webstorage due its larger attack surface, but to be honest I've seen examples of both methods. For single page applications you can also consider keeping the token in memory without persistent storage.

  • 一方。Mobile Applicationの場合は、専用の隔離されたStorage(OS Keychain/Keystore)へ保管するので、ブラウザほど懸念する必要はない
    • mobile appの場合には、おそらく Headerを利用する方法で問題ないのだろう (前の会社のプロジェクトでの認証サーバはResponse Bodyでtokenを返す方法だったので)

(上記は以下より引用)

https://security.stackexchange.com/questions/195474/jwt-or-session-cookie-for-api-for-both-web-and-mobile-app

XSSについて
Web Storage (localStorage/sessionStorage) is accessible through JavaScript on the same domain. This means that any JavaScript running on your site will have access to web storage, and because of this can be vulnerable to cross-site scripting (XSS) attacks.
Modern web apps include 3rd party JavaScript libraries for A/B testing, funnel/market analysis, and ads. We use package managers like Bower to import other peoples’ code into our apps.

mobile appに対する対応
For mobile apps you should store them in the OS's Keychain/Keystore (most likely through a wrapper) which is designed for such a purpose. Where to store JWTs on a browser on the other hand is still a rather controversial topic as storing in webstorage (sessionStorage/localStorage) is vulnerable to XSS-Attacks while storing inside a cookie is vulnerable to CSRF.

Cookie の場合

以下のCookie属性を正しく設定すれば、localStorage/sessionStorage + Http HeaderのSolutionよりも安全にJWTを管理できそう (上記にも書いた通りブラウザの場合は特に)。

なぜなら、、、

  • 同じドメインのリソースのみTokenを含んだCookieにアクセスできる
  • HttpOnly属性をつけることで、Scriptからのアクセスを遮断できる
    • XSS Attackより守られる
  • Secure 属性をつけることで、通信プロトコルがHTTPSの場合にのみCookieを送信するようにする
    • 通信の途中でCookie情報が読み取られたり改竄されるリスクがなくなる

(上記は以下より引用)
https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage

Cookies, when used with the HttpOnly cookie flag, are not accessible through JavaScript, and are immune to XSS. You can also set the Secure cookie flag to guarantee the cookie is only sent over HTTPS. This is one of the main reasons that cookies have been leveraged in the past to store tokens or session data.

とはいえ、以下のデメリット/懸念点も存在する。

  • RestFul APIはStatefulであるべきなのに、Statefulになってしまうのではないかという懸念
    • とはいえ、CookieはStatelessの状態で、あくまでJWTを保持するストレージとして利用することも可能なのである
    • なので、以降の調査内容は、Statelessなまま、JWTをCookie内に保持することができるか、というところにフォーカスしていく
  • CSRF攻撃の対象になってしまう
    • CSRFから保護する方法は存在する
    • なので、以下の調査内容は、JWTをCookieへ保持したまま、有効なCSRF対策を行うことはできるかどうかというところへフォーカスしていく

(上記も同様に以下より引用)
https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage

(Stateless APIに対して、Cookieがそぐわないと考えられている理由)
Modern developers are hesitant to use cookies because they traditionally required state to be stored on the server, thus breaking RESTful best practices. Cookies as a storage mechanism do not require state to be stored on the server if you are storing a JWT in the cookie.
(以下、CSRFについて)
However, cookies are vulnerable to a different type of attack: cross-site request forgery (CSRF).

なので、ここではStateless APIに対して、Cookieを利用したJWT Tokenの管理方法、及びその場合のCSRF対策についてチェックしていくことにする。 :notebook:

ズバリその実装方法とは!!

JWT,XSS, CSRFの基礎から具体的かつ丁寧に説明してくれる記事(https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage) 
ではあったが、肝心のCookieを利用したStateless API向けのJWT管理方法については、この数行の説明しかなかったので、何がなんだかわからなかった :astonished:

本当に何の前置きもなくズバリこの説明だけ :exclamation:

You can make this CSRF protection stateless by including a xsrfToken JWT claim:

{
  "iss": "http://galaxies.com",
  "exp": 1300819380,
  "scopes": ["explorer", "solar-harvester", "seller"],
  "sub": "tom@andromeda.com",
  "xsrfToken": "d9b9714c-7ac0-42e0-8696-2dae95dbc33e"
}

なので、ここからは別の LINKさん達にお世話になりました :heart_eyes:

Cookieで保管されたJWT TokenにCSRF対策を組み込む方法

① 認証サーバは、クライアント認証を完了させた後、ランダムなユニーク文字列を生成する
② そしてその値をxsrfToken JWT Claim属性としてセットする

つまり、認証サーバによって生成されたJWTはこんな感じ

{
  "sub": "hk",
  "xsrfToken": "cjwt3tcmt00056tnvcfvnh4n1",
  "iat": 1560336079
}

③ 認証サーバからJWTを受け取ったAPIクライアントは xsrfToken ClaimをHttp Request Headerへ重ね付けして、APIへリクエストを送信する

つまり、
1. APIクライアントはJWTをデコードし、xsrfTokenの値を読み取る (上記例では cjwt3tcmt00056tnvcfvnh4n1)
1. その後、APIクライアントは xsrfTokenの値 (cjwt3tcmt00056tnvcfvnh4n1)を X-XSRF-TOKEN Http Headerとして追加して、APIヘリクエストを送信
1. つまり、APIリクエストには xsrfTokenの値 (cjwt3tcmt00056tnvcfvnh4n1)を access_token cookie内 (=JWT内) と Http Header (= X-XSRF-TOKEN)の両方に重複して含んでいる事になる :point_left:

つまり、APIクライアントがAPIへ送るリクエストはこんな感じ

GET / HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Cookie: access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsInhzcmZUb2tlbiI6IjV0alJ0bEpZU2lTaCR3KnNZJW1hcGhBbnZEWUd1NnMwIn0.BpA85MrtO7U8UvAsSwuoQcTDKwMDqMGwA1y6pscskC0
Host: www.google.com
User-Agent: HTTPie/0.9.3
X-XSRF-TOKEN: cjwt3tcmt00056tnvcfvnh4n1

④その後、リクエストを受け取ったリソースサーバはX-XSRF-TOKEN Headerの値とJWT xsrfToken claimの値が同一であるかをチェックする

  • マッチしていれば、リクエストを進める
  • マッチしていなければ401(UnAuthorized)を返却する
  • このように、Request Headerの値とCookieの値がマッチしているかのチェックを行うCSRF対策のことをDouble Submit Cookie という

つまり

  • 認証サーバ側がCSRF Tokenを発行してJWT Tokenとしてクライアントに渡す
  • APIクライアントはCSRF HeaderとしてそのCSRF Tokenを従来のCSRF実装のようにして渡す
  • APIリソースサーバ側は単純にJWT Token (=Cookie) 内のCSRFとRequest HeaderのCSRF Tokenがマッチしているかチェックする

だけど、、、
- CSRF TokenをFE側が正しく管理しなかったどうなるんだろう。。
- 例えばCookieで保持すべきCSRF TokenなどをうっかりLocalStorageに保管しちゃったり
- そういうことがないようにするのは開発者側の役割何だろうな

とはいえ、、、

昨今はCSRF対策もTokenを利用する以外にも方法があるようだが、、

① Same-Site Cookies
https://scotthelme.co.uk/csrf-is-dead/

  • CSRF is dead! なんて言っちゃってる。。。w
  • それくらい良いSolutionなのだろう。Cookieに Same-Site Policy属性をつけるだけで、クロスドメインからのCookie送信を一切遮断できる

②Same Origin Pocily、Origin/Referer Headerでクライアントドメインをチェックする(もしくは同じDomainからのリクエストのみ許可する

個人的にはこれさえ対応していれば全く問題ないと思ったのだが、、

  • Same Origin Policyだけでは完全にCSRF脆弱性のリスクは減らせないと書いてある
  • 例えば、GET, HEAD and POSTの特定のリクエストヘッダとコンテントタイプを持つリクエスト (=simple requests)に対して、ブラウザはOrigin checkのためのPreflight checkをせずにリクエスト/レスポンスを通過させちゃう

If same-origin policy would work for all types of request then you would be right and there is no need to use CSRF token, because you would have full protection by the same-origin policy. However, this is not the case. There are a couple of HTTP requests that do not send a preflight request!

GET, HEAD and POST requests with specific headers and specific content-type do not send a preflight request. Such requests are called simple requests. This means the request will be executed and if the request was not allowed, then a not-allowed error response will be returned. But the problem is, that the simple request was executed on the server.

③ BrowserからAPIを直接コールせず、Browser用のApplication Serverを立てる
https://security.stackexchange.com/questions/61966/is-it-safe-to-skip-csrf-checks-for-non-browsers

最後の回答

Answering my own question.
省略・・
A core API for RESTful clients.
A browser API that acts as a bridge for browsers.
The core API does not implement any browser-specific features (e.g. cookies, CSRF checks).
The browser API mirrors the core API (exporting equivalent, browser-specific, methods where necessary).
It is a server-side implementation that runs browser-specific security checks, and converts browser-specific features to the format expected by the core API (e.g. cookies to JSON values).
It forwards requests to the core API and converts the response back to a browser-specific format (creating cookies as necessary).
An attacker cannot bypass security checks by accessing the core API because it ignores cookies. Without cookies, an attacker cannot carry out CSRF attacks.

要は、、、

  • ブラウザから認証サーバやリソースサーバを直接コールしない
  • 代わりに、ブラウザのリクエストやレスポンスをハンドリングする Web Application Serverを立てる
  • このWeb Appが
    • 認証サーバへTokenを取りに行く
      • (個人的には、取得したTokenをこの App Server内のSessionへ保持すれば安全だと思った)
    • ブラウザのリクエストをプロキシして、本来のAPIへリクエストを転送する
      • その際には、Sessionに格納されたTokenをHttp Header (Bearer Token)などとして送る
    • 必要な箇所にCSRF実装を行う (この App ServerはSessionを利用しているため)

そうすることによって
- JWTなどのセンシティブな情報をサーバ側に完全隠避できる
- Sessionを利用しているとはいえ、CSRF対策は通常通り導入できる

なので、一番わかりやすい実装なのではないかと思った :tada:

(前の会社ではは私がバックエンド開発者であったため、FEの実装をきちんとチェックできていないので、この仮説を立証できないのがつらいところ。。 :sweat_drops: )

自分なりの考察&まとめ

ユースケースによるのだろうけれども、もしWeb Applicationにおいて、JWTを安全に管理したい場合には、Cookieで保管 + CSRF対策をとったほうが良い ということだろう。

ただ、場合によっては、認証サーバとリソースサーバがどうしても異なるドメインということもあるだろう。Cross doamin cookieは不可能なので、その場合はどうしてもResponse Bodyから引き継いだ値を安全に保持するしかないのであろう (例えば、LocalStorage/SessionStorageへは保管しない、など)。

ただ、いずれにしても、ブラウザ側の実装がややこしくなってしまうので、ブラウザ用のプロキシリクエストができるアプリケーションサーバをたてて、センシティブな情報はブラウザ側へ持たせないようにさせるのが一番クリーンなのではないかと思った。

0
0
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
0
0