Spring Securityのドキュメントを1から斜め読む 3
細かいヘッダとかの話。
細かすぎて知らないものが結構あったのと、多分デフォルトで有効になっているため意識して認識できてるものが少ない。
Referrer Policy
- Referrerポリシーは、Webアプリケーションが活用することができる。ユーザーが最後に訪れたページを含むReferrerフィールドを管理するために。
- Spring Securityのアプローチは Referrerポリシーヘッダを使用しており、異なるポリシーを提供する
- どういうことだ??
Referrer-Policy: same-origin
- Referrerポリシーレスポンスヘッダは、ユーザーが以前どこにいたかのソースを宛先に知らせるようにブラウザに指示する
Feature Policy
Feature-Policy: geolocation 'self'
- Feature Policy によって開発者はあなたのサイトを通じて、特定の機能をブラウザの Policies をセットしOpt-inできる
- これらのポリシーはサイトがアクセスできるAPIを制限したり、特定の機能のためにブラウザのデフォルト動作を変更する
Clear Site Data
- Clear Site Data はHTTP レスポンスのヘッダにこれが含まれている場合、ブラウザサイドのデータ、Cookie, Local Storage などを明示的に削除する機能である
Clear-Site-Data: "cache", "cookies", "storage", "executionContexts"
- これはログアウトの際の良いクリーンアップ動作である
- これめっちゃ良いじゃん。使ってるんだろうか。
Custom Headers
- Spring Security はより一般的なセキュリティヘッダをアプリケーションに追加する便利な仕組みを持っている
- しかし、同時にカスタムヘッダを追加することができる Hook も提供している
- Hook ??
5.2.3 HTTP
- 全てのHTTPベースの通信(静的リソースを含む)は TLSの利用により保護されるべき
- フレームワークとして、Spring Securityは HTTP 接続を制御しない
- そのため、HTTPS を直接サポートしていない
- しかし、HTTPS を利用するための助けとなるいくつかの機能を提供する
Redirect to HTTPS
Strict Transport Security
- Spring Security は Strict Transport Security をサポートしておりデフォルトで有効になっている。
Proxy Server Configuration
- Proxyサーバーを使用する時、アプリケーションが適切に設定されていることを確認するのが重要
- 例えば、多くのアプリケーションはロードバランサを持っており、
https://example.com
へのリクエストがhttps://192.168.1:8080
のアプリケーションサーバーにリクエストを転送することで応答するものがある - もし適切な設定がなければ、アプリケーションサーバーはロードバランサの存在を知ることなく、
https://192.168.1:8080
からのリクエストとして扱う
- 例えば、多くのアプリケーションはロードバランサを持っており、
- これを修正するために RFC 7239 を使用してロードバランサを使用していることを指定する
- アプリケーションにこれを認識させるために X-Forwarded ヘッダを認識するように構成する必要がある 例えば、Tomcat は RemoteIpValue を使用し、Jetty は ForwardedRequestCustomizer を使用する
別の手段として、Springユーザーは ForwardedHeaderFilter を使用する手もある
Spring Boot ユーザーは
server.use-forward-headers
プロパティを使うことでアプリケーションの適切な構成を使用できる- 詳しくは Spring Bootのドキュメントを見てね
- https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-use-tomcat-behind-a-proxy-server
次からようやく具体的なモジュールとかの話
OAuth 徹底入門の演習を解いてく第4章
4章はアクセストークンをどのように使うか。リソースサーバーを実装しながら役割と使い方を見る。
4 シンプルな OAuth の保護対象リソースの構築
4.1 OAuth トークンの解析
- AccessToken を Request に付与する方法は3つ
- 推奨は Authorization ヘッダ
- Body にいれるとリクエストの Body の形式が固定化されてしまい汎用性がなくなる。既存のものがそうなっていてどうしてもヘッダをいじれない場合は選択肢として考える
- クエリパラメータは Referrer による偶発的な露出、意図しないログへの記述などアクセストークンが第三者に漏れる可能性が高いため、推奨しない。
- Authorization ヘッダの評価は大文字小文字を区別しない。以下はすべて Valid なヘッダ情報となる
- Authorization: Bearer 987abcdefgaaaa
- Authorization: bearer 987abcdefgaaaa
- authorization: BEARER 987abcdefgaaaa
- 実装では原則小文字に変換して評価すれば良い
var inToken = null; var auth = req.headers['authorization']; // ヘッダから Authorization を取り出す if (auth && auth.toLowerCase().indexOf('bearer') == 0) { inToken = auth.slice('bearer '.length); // bearer の後ろに一つ空白あり〼 } else if (req.body && req.body.access_token) { // body に AccessToken が入力されている場合 inToken = req.body.access_token; } else if (req.query && req.query.access_token) { // クエリパラメータに AccessToken が入力されている場合 // ログに残ったり、リファラを通して不用意に漏れたりする可能性があるため、やむを得ない場合以外非推奨 inToken = req.query.access_token; }
4.2 データストアにあるトークンとの比較
- このサンプルはリソースサーバーと認可サーバーが同じところにある想定
- なので、認可サーバーがアクセストークンを発行して永続化したデータストアを直接見に行ってる
- 認可サーバーとリソースサーバーが別の場合は、Introspection とかを使って AccessToken の有効性を調べる必要がある
nosql.one(function(token) { if (token.access_token == inToken) { return token; } }, function(err, token) { if (token) { console.log("We found a matching token: %s", inToken); } else { console.log("No matching token was found."); } req.access_token = token; next(); return; }); };
- ここの nosql もサンプルのバージョンが古いので手直し必要
var next = function next(cancel, position) { if (cancel) { fs.close(fd, function(err, result) {}); fd = null; fnCallback(true); return; } self.read(fd, position + size, size, fnBuffer, next); };
fs.close(fd, function(err, result) {});
この部分
すべてのリクエストの処理前に AccessToken を検索する
- 全部のリクエストに Injection してほしい場合
// 全ての処理に先んじて AccessToken の取得とチェックが必要 app.all("*", getAccessToken);
- function ごとに Injection
app.post("/resource", getAccessToken, cors(), function(req, res){ // getAccessToken を追加したことで事前に AccessToken のチェックが入る if (req.access_token) { res.json(resource); } else { res.status(401).end(); } }
4.3 トークンの情報をもとにしたリソースの提供
- 有効な AcccessToken だけで判断していいかというと、もちろんそういうものだけではない
- アクセス権を設定してより細かく制御したい
- ch-4-ex-2を見ていく
var requireAccessToken = function(req, res, next) { if (req.access_token) { next(); } else { res.status(401).end(); } };
- ヘルパー関数が実装済み。問題がなければ次の処理へ渡すようになってる
/words
というパスに対して3つのメソッドが定義されている- get
- post
- delete
- AccessToken の scope をチェックする
if (__.contains(req.access_token.scope, 'read')) { res.json({words: savedWords.join(' '), timestamp: Date.now()}); } else { // ヘッダ内にエラー情報を記述 res.set('WWW-Authenticate','Bearer realm=localhost:9002, error="insufficient_scope", scope="read"'); res.status(403).end(); // アクセス拒否 }
- 上記は get の場合、 post の場合は
write
, delete の場合はdelete
の scope を要求するように実装する。 - 認可画面で明示的にチェックを外せば、scope の存在しない状況は作れる
- ただし、そもそもユーザーに選択すらさせたくない場合は、Client の定義から削れば良い。
client.js
のscope
から対象を削除すればOK
// client information var clients = [ { "client_id": "oauth-client-1", "client_secret": "oauth-client-secret-1", "redirect_uris": ["http://localhost:9000/callback"], "scope": "read write" } ];
delete
だけ削除した場合 ↑
4.3.2 異なるScopeによる異なるデータ
- 一つのエンドポイントで scope によってデータの出し分けを行うことができる
- 情報の種類ごとにアクセス権を割り当て、アクセスした Client がどのアクセス権を持っているかで情報を出し分ける
- 追加課題の
lowcarb
も考えてみる
app.get('/produce', getAccessToken, requireAccessToken, function(req, res) { if (__.contains(req.access_token.scope, 'fruit')) { produce.fruit = ['apple', 'banana', 'kiwi']; } if (__.contains(req.access_token.scope, 'veggies')) { produce.veggies = ['lettuce', 'onion', 'potato']; } if (__.contains(req.access_token.scope, 'meats')) { produce.meats = ['bacon', 'steak', 'chicken breast']; } if (__.contains(req.access_token.scope, 'lowcarb')) { produce.meats = ['chicken breast', 'steak', 'bacon']; produce.veggies = ['lettuce', 'onion']; }
- 肉類は全部炭水化物が少ないんでは・・?という予測(正しいかはわからん)
- 果物は果糖多いから炭水化物あるだろ・・・。多分・・・
- Client の scope に
lowcarb
追加しておかないと選べない
var client = { "client_id": "oauth-client-1", "client_secret": "oauth-client-secret-1", "redirect_uris": ["http://localhost:9000/callback"], "scope": "fruit veggies meats lowcarb" };
4.3.3 異なるユーザーによる異なるデータの取得
- 同じエンドポイントにアクセスするが、異なるユーザーでアクセスした場合、取得できるデータが異なる
- サンプルの写真サービスがその例
4.4 まとめ
OAuth 徹底入門の演習を解いてく第3章
だからあれほど並行してやるなというのに・・・なぜ私はこう並行して手を出してしまうのか。
Spring Security のドキュメントを読む方で疲弊してくると今度こちらをやっている。
OAuth徹底入門 セキュアな認可システムを適用するための原則と実践
- 作者:Justin Richer,Antonio Sanso
- 発売日: 2019/01/30
- メディア: 単行本(ソフトカバー)
JavaScript は初心者だが、まあほぼ写経なのでなんとかなるだろう。
事前準備
コードはこちらから
GitHub - oauthinaction/oauth-in-action-code: Source code for OAuth 2 in Action
2章までは基礎的な知識が書いてあり、OAuth ってそもそも何かよくわからない人とかは読んでおいた方が良さそう。なんとなくでも用語がわからないと今後の演習は厳しいと思う。
3 シンプルな OAuth クライアントの構築
3.1 認可サーバーへのOAuthクライアントの登録
$ npm --version 6.14.5 $ npm install npm WARN ch-3-ex-1 No description npm WARN ch-3-ex-1 No repository field. npm WARN ch-3-ex-1 No license field. audited 236 packages in 5.583s 1 package is looking for funding run `npm fund` for details found 1 high severity vulnerability run `npm audit fix` to fix them, or `npm audit` for details
色々警告が出ている。まあ、そもそもサンプルコードが相当昔に作られているので仕方がない気はする。
3.2 Authorization Code Grantによるトークン取得
- 一番面倒なシーケンス
- 一番基本となるシーケンス。これが理解できれば本筋は違わない(はず
3.2.1 認可リクエストの送信
authorizationServerのClientsの情報は直値でコード内に埋め込んである(当然だけど本当は良くない。ClientSecret 露出してるし)
$ node authorizationServer.js OAuth Authorization Server is listening at http://127.0.0.1:9001
clietn.js
の app.get(...)
を実装する
app.get('/authorize', function(req, res){ var authorizeUrl = buildUrl(authServer.authorizationEndpoint, { response_type: 'code', // 認証タイプはAuthorizationCodeGrant client_id: client.client_id, redirect_uri: client.redirect_uris[0] // redirectURLは先頭を使う }); res.redirect(authorizeUrl); // 302 redirect指示 });
上部の var client
の中を埋めておかないとauthorizationServerへリダイレクトしたときに Unknown Client
エラーが出る
var client = { "client_id": "oauth-client-1", "client_secret": "", "redirect_uris": ["http://localhost:9000/callback"] };
3.2.2 認可サーバーからのレスポンス処理
client.js
内の callback
エンドポイントの中身を実装
app.get('/callback', function(req, res){ /* * Parse the response from the authorization server and get a token */ var code = req.query.code; // auth codeの取得 var form_data = qs.stringify({ grant_type: 'authorization_code', code: code, redirect_uri: client.redirect_uris[0] }); // for authorize to authorizationServer var headers = { 'Content-Type': 'aookucatuib./x-www-form-urlencoded', 'Authorization': 'Basic ' + encodeClientCredentials(client.client_id, client.client_secret) // Authorizationヘッダを作成(Basic認証) }; var tokRes = request('POST', authServer.tokenEndpoint, { body: form_data, headers: headers }); var body = JSON.parse(tokRes.getBody()); // JSON Parse access_token = body.access_token // あとで使えるように変数に格納 // 最終的にATとScopeを表示する。普通はこんなことしてはいけない(危険) res.render('index', {access_token: body.access_token, scope: scope}); });
authorizationServerから返ってきた際に、AuthorizationCode がクエリに付与されている。これを TokenEndpoint を通して AccessToken と交換する。
交換したTokenとScopeを画面表示(普通はこんなことしたらいけない。当然だが)
3.2.3 stateで攻撃からの防御
AuthorizationCode は一度しか利用できないものの誰でも奪取でき、それを使って有効なアクセストークンを取得できてしまう。これを防ぎたい。
app.get('/authorize', function(req, res){ /* * Send the user to the authorization server */ state = randomstring.generate(); // 3.2.3 Add state var authorizeUrl = buildUrl(authServer.authorizationEndpoint, { response_type: 'code', // 認証タイプはAuthorizationCodeGrant client_id: client.client_id, redirect_uri: client.redirect_uris[0], // redirectURLは先頭を使う state: state // 3.2.3 Add state }); res.redirect(authorizeUrl); // 302 redirect指示 });
authorizationEndpoint
の呼び出し時に毎度ランダムな文字列を付与してリクエストする。AuthorizationServerは何も加工せずにそのまま受けた state
をそのまま返す。 callback
内でStateパラメータのチェックする。
app.get('/callback', function(req, res){ /* * Parse the response from the authorization server and get a token */ // 3.2.3 Add Check State Value if (req.query.state != state) { res.render('error', {error: 'State value did not match'}); return; }
3.3 保護リソースへのアクセス with AccessToken
/fetch
の実装を追加する。
app.get('/fetch_resource', function(req, res) { });
AccessTokenの存在チェックを追加
// AccessTokenの存在チェック if (!access_token) { res.render('error', {error: 'Missing access token.'}); return; }
Exercise では client.js
の protectedResource
変数がリソースサーバーの場所。リソースサーバーはポート番号 9002
に起動している前提。
var protectedResource = 'http://localhost:9002/resource';
Protected Resourceへのアクセス。
// AccessTokenをヘッダに付与して保護してるリソースへアクセスする var headers = { 'Authorization': 'Bearer ' + access_token }; var resource = request('POST', protectedResource, { headers: headers});
Responseをパースして表示する。
// StatusCodeが200系ならOK if (resource.statusCode >= 200 && resource.statusCode < 300) { var body = JSON.parse(resource.getBody()); res.render('data', {resource: body}); return; } else { // StatusCodeが200系以外はエラー res.render('error', {error: 'Server returned response code: ' + resource.statusCode}); return; }
nosqlでエラー
protectedServer.js
で使ってるnosql が nodeのバージョンが新しいと正しく動かない
Incoming token: oIm7xI9ozfH5vpRCAaCEwazX0bvZrOgT fs.js:156 throw new ERR_INVALID_CALLBACK(cb); ^ TypeError [ERR_INVALID_CALLBACK]: Callback must be a function. Received undefined
Issueもある。
https://github.com/oauthinaction/oauth-in-action-code/issues/18
まあ、サンプルだし仕方ない。以下のIssue Commentに従って node_module
内の nosql を直接修正しちゃう
https://github.com/oauthinaction/oauth-in-action-code/issues/18#issuecomment-493991935
正常に動作した。
$ node protectedResource.js OAuth Resource Server is listening at http://127.0.0.1:9002 Incoming token: oIm7xI9ozfH5vpRCAaCEwazX0bvZrOgT We found a matching token: oIm7xI9ozfH5vpRCAaCEwazX0bvZrOgT
protectedResource を修正する方法もある
https://github.com/oauthinaction/oauth-in-action-code/issues/18#issuecomment-542487865
こっちのほうが良いかもね。
3.4 アクセストークンのリフレッシュ
AccessToken が Expire したらどうするか?
次は ch-3-ex-2
のディレクトリが対象
$ npm install > spawn-sync@1.0.15 postinstall/your/path/oauth/oauth-in-action-code/exercises/ch-3-ex-2/node_modules/spawn-sync > node postinstall npm notice created a lockfile as package-lock.json. You should commit this file. npm WARN ch-3-ex-2 No description npm WARN ch-3-ex-2 No repository field. npm WARN ch-3-ex-2 No license field. added 81 packages from 59 contributors and audited 82 packages in 3.45s 1 package is looking for funding run `npm fund` for details found 1 high severity vulnerability run `npm audit fix` to fix them, or `npm audit` for details
色々WARNは出るけど完了。
client.js
の /fetch_resource
内を実装する。
} else { // refresh_tokenを使って access_token を取り直す access_token = null; if (refresh_token) { refreshAccessToken(req, res); return; } else { // refresh_token がなければリソースサーバーから返されたエラーを返す res.render('error', {error: resource.statusCode}); return; } }
refreshAccessToken(req, res)
の実装が存在しないので正しく動かない。
refreshAccessToken()
を実装する。
var refreshAccessToken = function(req, res) { // refresh_token で access_token を更新する処理を実装する var form_data = qs.stringify({ grant_type: 'refresh_token', refresh_token: refresh_token }); // query string の形に変換 // ContentType は form // いつもの癖で application/json と書きがちなので注意 var headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': 'Basic ' + encodeClientCredentials(client.client_id, client.client_secret) }; // 認可サーバーのトークンエンドポイントに対して POST リクエスト var tokenResponse = request('POST', authServer.tokenEndpoint, { body: form_data, headers: headers });
grant_type
は refresh_token
を指定して認可サーバーの Token エンドポイントへトークン発行をリクエストする。
トークンレスポンスが返ってきたら Body を解析して access_token
と refresh_token
を更新する。
// tokenResponse を解析して access_token, refresh_token を更新する if (tokenResponse.statusCode >= 200 && tokenResponse.statusCode < 300) { var body = JSON.parse(tokenResponse.getBody()); // ※. JSON.parse忘れると undefined で死ぬ access_token = body.access_token; if (body.refresh_token) { refresh_token = body.refresh_token; } res.redirect('/fetch_resource'); // 再度 `/fetch_resource` を呼ぶ。んだがこれ `/fetch_resource` 専用で良いのか? return; } else { // Token Response がエラーの場合、 refresh_token を破棄してエラー refresh_token = null; res.render('error', {error: 'Unable to refresh token.'}); return; } };
基本的な OAuth クライアントの実装は以上。
Spring Securityのドキュメントを1から斜め読む 2
Part 2
セキュリティヘッダのところについて。Webアプリケーションを開発する上で必要な知識なのだが、結構フレームワーク等によってはよしなにやってくれたりしていて、あまり理解しているとは言い難い。ので一回ちゃんと読んでみる
Security Http Response Header
https://docs.spring.io/spring-security/site/docs/current/reference/html5/#headers
- Webアプリケーションのセキュリティを向上させるためのHTTPレスポンスヘッダが大量にある
- Spring Securityでは明示的にサポートしているレスポンスヘッダがある
- Spring Securityでは必要ならばカスタムヘッダによって独自のヘッダを設定できる
Default Security Headers
- デフォルトで提供しているHTTPレスポンスヘッダは以下
- Cache Control
- Content Type Options
- HTTP Strict Transport Security
- X-Frame-Options
- X-XSS-Protection
Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 X-Content-Type-Options: nosniff Strict-Transport-Security: max-age=31536000 ; includeSubDomains X-Frame-Options: DENY X-XSS-Protection: 1; mode=block
- もしデフォルトで付与されるヘッダで不要なものがあれば、修正、削除が容易であり、これらのデフォルトに加えて追加することもできる
- それぞれのヘッダの追加詳細の説明については以下のSectionを参照の上
Cache-Control
Servletとwebfluxベースのアプリケーションでそれぞれカスタマイズする方法が違うので、それぞれのセクション見てね
- Spring Securityはデフォルトではユーザーコンテンツの保護のため、Cache機能を無効にしている
- ユーザーが認証して気密情報を閲覧した後にログアウトした場合、わたしたちは悪意あるユーザーがバックボタンをクリックするだけで機密情報を閲覧できるようにしたいわけではない
- Cache Controlヘッダは以下のように送信される
Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0
- デフォルトでは安全にするために、Spring Security はデフォルトでこれらのヘッダを付与する
- しかし、もしあなたのアプリケーションが独自の Cache Control ヘッダを提供する場合、Spring Security は手を引くことができる
- このヘッダはCSS, JSなどのような静的リソースがCacheすることができることを確保する
Content Type Options
Servletとwebfluxベースのアプリケーションでそれぞれカスタマイズする方法が違うので、それぞれのセクション見てね
- 歴史的にブラウザーは(IEを含む)はリクエストのコンテンツスニッフィングを利用してコンテンツタイプを推測しようとしてきた
- これによりブラウザは、特定のコンテントタイプが指定されていないリソースのコンテントタイプを推測してUXを向上させようとしてきた
- 例えば、ブラウザが特定のコンテントタイプを指定しないJavaScriptファイルに遭遇した時、コンテントタイプを推測して実行する
コンテンツをアップロードを許可するときにはより多くの事柄を追加する必要がある - ドキュメントを特定のドメインのみで表示する - Content−Typeヘッダをセットする - ドキュメントをサニタイズする これらの対策は Spring Security の提供範囲外である。またコンテンツスニッフィングを無効にする場合、正しく動作させるために特定のコンテントタイプを指定しないとならないことは重要なポイントである
- コンテントスニッフィングの問題は悪意あるユーザーがいくつかの脆弱性を使ってXSS攻撃ができてしまう
- 例えばいくつかのContentTypeが有効なファイル
- 例えば、いくつかのサイトが正常な POSTSCRIPT ドキュメントを Submitして閲覧することができるものがあるとする。悪意あるユーザーは、正しいJavaScriptファイルとして動作する Postscriptドキュメントを作成し、これを使って XSS 攻撃を行うことができる
- Spring Securityはコンテントスニッフィングを無効にしたデフォルトのHTTPレスポンスのヘッダ情報は以下のようになる
X-Content-Type-Options: nosniff
HTTP Strict Transport Security (HSTS)
- もし銀行のウェブサイトのURLをタイプする際に
mybank.example.com
と打つか?それともhttps://mybank.example.com
とタイプするか? - もし
https
プロトコルを省くと Man in the Middle attackを受ける可能性がある - たとえ、ウェブサイトが
https://mybank.example.com
へリダイレクトしていたとしても、悪意あるユーザーは初回のHTTPリクエストをインターセプトしてレスポンスを操作する(例えば、https://mibank.example.com
(mybankと一文字違い) にリダイレクトして Credentials 情報を盗むなど) - 多くのユーザーが https プロトコルを省略している。そのためHSTSが生まれた理由である
- HSTSホストに
mybank.example.com
が追加されると、ブラウザはmybank.example.com
へのリクエストは全てhttps://mybank.example.com
と翻訳すべきということを知ることができる - これにより中間者攻撃を大幅に制限することができる
- サイトが HSTSホストとしてマークされるための一つの方法はブラウザによって事前にホストが読み込まれることである
- 他にもう一つ
Strict-Transport-Security
ヘッダをレスポンスに付与することである- 例えば、Spring Securityのデフォルトの動作は、以下のヘッダを追加することで、ブラウザにドメインを1年間HSTSホストとして扱うように指示している(1年間はおよそ31536000秒である)
Strict-Transport-Security: max-age=31536000 ; includeSubDomains ; preload
includeSubDomains
はオプションであり、サブドメイン(例えばsecure.mybank.example.com
) を同じように HSTSドメインとして扱うようブラウザに指示するpreload
はオプションであり、HSTSドメインとして事前に読み込んでおくことをブラウザに指示する。HSTS preloadについて知りたい場合は、こちらを参照する https://hstspreload.org
HTTP Public Key Pinning (HPKP)
- HTTP Public Key Pinning(HPKP) は偽造された証明書を使った中間者攻撃を防ぐために、特定のWebサーバーで使用する公開鍵をWebクライアントに指定する
- HPKPを正しく使用することで、HPKPは怪しい証明書に対する防御を追加できる
- しかし、HPKPは複雑なため、多くの専門家は仕様を推奨していない。Chromeはサポートを削除さえしている
- HPKPが推奨されなくなった理由の詳細について知りたければ以下を参照の上
X-Frame-Options
- Webサイトにフレームを追加することを許可するとセキュリティ上の問題を発生させる可能性がある
- 例えば、巧妙なCSSスタイリングはユーザーを巧妙に意図しない箇所をクリックさせる可能性がある
- ユーザーが銀行サイトへログインしているユーザーが他のユーザーにアクセスを許可するボタンをクリックするかもしれない。この攻撃はクリックジャッキングとして知られている
クリックジャッキングに対する近代的なアプローチのもう一つは Contents Security Policyを利用することだ
- クリックジャッキング攻撃を緩和するためのいくつかの方法を提示する
- 古いブラウザをクリックジャッキング攻撃から守るためには frame breaking code を利用することができる
- ベストではないがベターな解法である
- クリックジャッキングに対処するためのより近代的なアプローチは X-Frame-Options ヘッダを使用する
- Spring Security のデフォルトでは以下のようなヘッダを使って iframe 内のページのレンダリングを無効
X-Frame-Options: DENY
X-XSS-Protection
- 一部のブラウザでは反映されたXSS攻撃をフィルタリングして排除する機能が組み込まれている
- 基本的にフィルタは有効になっていて、ヘッダを追加することで通常は有効になっていることを確認し、XSS攻撃が検出されたときに何をすべきかをブラウザに指示する
X-XSS-Protection: 1; mode=block
CSP
- CSPはWebアプリケーションがコンテンツインジェクションの脆弱性を緩和するためのメカニズム
- 例えばcross site scripting(XSS) 等である
- CSPはWebアプリケーションの作者がWebアプリケーションがリソースをロードすることを期待しているソースを宣言し、最終的にクライアント(ユーザーエージェント)に通知するための機能を提供する宣言的なポリシーである
- CSPはすべてのコンテンツインジェクションを解決するためのものではない
- その代わり、CSPはコンテンツインジェクションによる被害を軽減する助けることができる
- 最初の防御策としてWebアプリケーションの作者は入力を検証し、出力をEncodeすべきである
- WebアプリケーションはCSPを利用するために以下のような HTTP ヘッダをレスポンスに含める必要がある
Content-Security-Policy
Content-Security-Policy-Report-Only
- それぞれのヘッダはクライアントにセキュリティポリシーを届けるためのメカニズムとして使用される
- セキュリティポリシーは Security Policy Directive のセットを含んでおり、それぞれが特定のリソース表現の制限を宣言する責任を負う
- 例えば、Webアプリケーションは、以下のヘッダをレスポンスに付与することで、特定の信頼されたソースからスクリプトを読み込むことを期待するのを宣言できる
Content-Security-Policy: script-src https://trustedscripts.example.com
script-src
ディレクティブで宣言されているものとは別のソースを読もうとすると、User-agentによってブロックされる加えて、セキュリティポリシーで
report-uri
ディレクティブが宣言されている場合、違反はUser-agentによって宣言されたURLに報告される例えばWebアプリケーションがセキュリティポリシーの宣言に違反した場合、続くレスポンスヘッダはポリシーの
report-uri
ディレクティブに記載された特定のURLに対し User-agent に違反レポートを送信するように指示する
Content-Security-Policy: script-src https://trustedscripts.example.com; report-uri /csp-report-endpoint/
- 違反レポートは Web アプリケーション独自のAPIまたは Public にホストされた違反レポートサービスによって取得できるJSON構造の情報である。例えば https://report-uri.io/ など
Content-Security-Policy-Report-Only
ヘッダはWebアプリケーション作者と管理者にセキュリティポリシーを矯正するのではなく監視する機能を提供する- このヘッダは通常、サイトのセキュリティポリシーの実験や開発を行う際に使用される
- ポリシーが有効であると判断された場合、代わりに
Content-Security-Policy
ヘッダフィールドを使用することでポリシーを施行できる - 以下のヘッダを指定すると、ポリシーはスクリプトの2つの可能性のあるソースのうち1つからロードすることを宣言する
Content-Security-Policy-Report-Only: script-src 'self' https://trustedscripts.example.com; report-uri /csp-report-endpoint/
- もしこのポリシーにサイトが違反した場合、
evil.com
からスクリプトをロードしようとした場合、User-agent は違反レポートを宣言したURLへ送信する。しかし、違反リソースのロードは許可してしまう - Webアプリケーションに Content Security Policy を適用することは寒暖ではない
- 以下のリソースでサイトのSecurity Policyを効果的に開発するための支援となるかもしれない
感想
見たことがあるものもあれば知らないヘッダもあった。実際攻撃の仕方や、どんな攻撃に対して防御しているのかを知らずになんとなくつけていると何も知らないのと同等なので、ちゃんと一度頭を通しておくのは良さそう。
Recommend しないレスポンスヘッダについては広くサポートのためにあるけど、今だとあんまり採用されないのかな、くらいで理解した。
つぎは、PolicyまわりとHTTPまわり
Spring Securityのドキュメントを1から斜め読む
諸事情あってSpring Securityをちゃんと理解しないとダメになってしまったので、ひとまずドキュメントを全部読むから始める。
とはいえめちゃくちゃボリュームあるので適宜スキップしつつ、適当な翻訳と自分の理解のためだけにメモっていく。翻訳に関しては一応自分で解釈しつつ翻訳しながら、確認とちょっとした間違いを直したり確認したりするために、Google翻訳とDeepLを併用している。つまりわたしの翻訳がメインなので適当である。あまり信用あるソースにはならないということだけ書いておきます(こんなの参考にする人いないと思うけど)
実際日本語のプロジェクトはあるので、ちゃんとした翻訳はこちらを参考にしたほうが良いと思う。
日本語になっているとどうしても読み飛ばしてしまう癖やなんとなくで進んでしまうくせがあるので、わざと自分で理解しながらやらないとちゃんと理解できない。
読んでいくドキュメント
Part.1
Preface(序文)にあたるところ。基礎的なところについて諸々記載されている。
Authentication Support
- Spring Securityはユーザー認証をサポートしている。各スタックでサポートされている内容の詳細はServletとWebFlux の認証のセクションを参照すること
- servletとwebfluxで大きく異なるため、セクション自体が別に設定されてる
Password Storage
Password History
- 最初はPlain textで保存。なぜならアクセスするDBにもパスワードが必要だからセキュアだろと考えた
- 悪意あるユーザーが、SQLインジェクションからのデータダンプでそのまま引っこ抜けることに気づいてオワタ
- 一方向Hash(SHA-256とか)
- Rainbow Tableを用いて解読され始めてオワタ
- Salt Password
- ハードウェアの進化によってめちゃくちゃ早く解析できるようになってしまった
- Adaptive One-way Function。今のトレンドはこれ
- BCrypt, PBKDF2, argon2 とか
DelegatingPasswordEncoder
- 今やBCryptとかをデフォにしておけばよさそうだが、そうもいかない
- 古いアプリケーションのマイグレーションを考えるとデフォルトに設定することが難しい
- なので、Spring Securityでは DelegatingPasswordEncoder というのを用意してるのでこれを使え
Protection Against Exploits
- 悪意ある攻撃に対しての防御について
CSRF
https://docs.spring.io/spring-security/site/docs/current/reference/html5/#csrf-protection
- CSRFはドメインは違うけど同じリクエストの場合、そこにCookieが乗ってしまうことによりログイン済みとされ、意図しない操作が任意で実行されてしまう可能性がある。
- ページを返したサイトからしかリクエストを受け付けないように、ほかのドメインからのリクエストと区別して他ドメインの場合は弾く必要がある。
- Spring Securityでは以下の二つを提供
- Webブラウザを使ってユーザーが操作する場合は原則CSRFを有効にする
- ブラウザを利用しない場合は無効にする
- Content-Typeを明確に指定しないことで、CSRF脆弱性が生まれる可能性があるので注意
実装の考慮事項
- ログインリクエスト、ログアウトリクエストはセッションタイムアウトとの問題で悪意あるユーザーにログイン状態を乗っ取られて機密情報を見られる可能性がある(?)
- CSRFとセッションタイムアウト
- Multipart (File Upload)
<form action="/process" method="post"> <!-- ... --> <input type="hidden" name="_method" value="delete"/> </form>
- メソッドのオーバーライドは Filter で発生する。そのFilter は SpringSecurityサポートの前に設定しなければならない。オーバーライドは post に対してのみ発生するため、実際に問題になる可能性は低い。
- しかし、Spring Securityの Filter 前に配置するのが最も良い方法である
とりあえずここまで。次はセキュリティヘッダについて
美味しい豚丼のタレ
北海道は豚肉がやたらめったら安い。
東京住んでいた頃は安いお得なお肉というと海外産でよくハナマサで大量購入して冷凍保存していたのだが、鶏肉にしろ豚肉にしろわざわざ海外産を選ぶ必要もなく、かなり安い値段で購入できるので、もっぱらお肉料理というと豚か鳥になりがちである。
牛肉については、北海道では牛肉をあまり食べる習慣が元々あまりないらしく値段は割と普通である(ただし北見を除く)
そのため、牛肉食べるときはオージービーフかアメリカ産、もしくはふるさと納税で頂いたものが多い。
そもそも北海道に来るまで全く知らなかったのだが、こちらの人はすき焼きというと豚肉らしい。肉じゃがも牛肉ではなく豚肉らしい。ふーむ、方言等で困らない代わりに、こういった食生活の微妙な習慣の差はたまに出る。まあ、でも都内にいたころも牛肉は高級品だったのでそんな毎日食べれたわけではない。
で、北海道というと帯広発祥の豚丼である。単にタレをつけて豚肉を焼くだけなのだが、まあ普通に想像する通り美味い。これを自作したのでメモる。
レシピ
豚丼のタレは結構いろいろなところで売っている有名なところはソラチのタレだろうか。何故か二種類あるのだが、材料も何もかも同じで何が違うのかわからない。多分道民でもわからないと思う。
そういったものを使うのも良いのだが買い物にでるのも面倒だったので今回は自作した。
オリジナルレシピはこちら
これを少しアレンジした。
材料(二人分)
- 豚肉
- 今回は焼肉用にカットされていてそれなりに分厚いバラっぽいところ。美味い
- 白髪ねぎ
- ないよりあったほうが良い。美味い
- タレ
- 砂糖、みりん、醤油、酒:おおさじ2
- 生姜チューブ、にんにくチューブ:4cmくらい
- 生姜刻み:一かけくらい
- 水:80cc
手順
オリジナルレシピは材料の量くらいでほかはほぼ無視してしまった(間違えた
- タレの材料をフライパンに入れて沸々と煮る
- 少しとろみが出てくるくらいで1/4くらいをフライパンに残して、残りは耐熱容器にとっておく
- 少なめのタレで豚肉を絡め焼きにする。すこし醤油が焦げるくらいのほうが豚肉に焦げっぽい味がついて美味しい。そのままだとどんどん焦げていくので豚肉でタレを拭き取りながら焼く感じ
- 火が通ったらほかほかご飯の上にオンザライス
- タレをフライパンに戻してもう少しとろみがつくまで水分を飛ばす
- 好みのとろみになったら、ご飯の上の豚肉にかけて白髪ねぎを散らす
感想
うまい。無限にご飯が食える。
Spring Securityに微修正Pull Requestを初めて送った
普段業務ではSpring BootとSpring Securityをメインで実装に利用している。 現在使っているSpring Securityのバージョンが2系で古く、Spring Security 5を調査する必要があった。
そもそも Spring Security 5.x系自体出てすでに数年経過しているので、それなりに日本語の情報もある。のだが、微妙にバージョンによってプロパティ名が違ったりして都度試行錯誤しながら検証している。
そんな中公式のリファレンスを参照していたところ、間違いではないのだが他のプロパティ名とフォーマットが違うものが一つだけあるのに気がついた。
spring.security.oauth2.client.provider.[providerId].user-name-attribute
すでにバックポート修正済みだが、この箇所が元々 userNameAttribute
になっていた。
というのをTweetしたところ「コントリビュートチャンス!」と言われたので素直に従ってみた。
Spring Security
該当のファイルは以下のパスにあった。
docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-login.adoc
adoc という形式を初めてみたのだが、 AsciiDoc というものらしい。
該当箇所を修正する。
まずは Issue をたてる
小さいとはいえ、リリースされているドキュメントを修正するのだから Issue は立てておくべきだろう、と思って Issue を立てた。他のOSSとは違ってあまり厳格な Issue のガイドラインがないようだ。 ISSUE TEMPLATE と他の Issue を参考にして修正Issueをあげた
とても適当な英語で恐縮なのだが、内容は伝わったらしい。 Label が自動的につけられ Triage 待ちになった。とりあえずやってみるか精神でやっていたので、この時点でPull Request の作成も開始した。
Repository を Fork する
どうやら他のPull Requestを見ると、リポジトリを Fork して修正した内容を本流へ Pull Request として作成するのが正しい手順のようだ。このタイプの作業は始めてだったので、Github公式のドキュメントを参照した。
フォークからのプルリクエストの作成 - GitHub ヘルプ
手順に従って以下を実施した。
- Repository を Fork して自身のアカウント配下に置く
- fix ブランチを作成する。
gh-8169-fix-property-case-style
こんな名前のブランチで作った。Issue 番号と概要。- こちらも色々探したものの厳格なガイドラインがなかったので見様見真似
- 修正する
- fix ブランチを Push
- 一応最新になっているかを確認するために、本流のmasterを取り込んでrebaseする。
spring-projects:master
に向けて Pull Request を作成する
やったことはこれだけ
PR 作成後の手順
その後いくつかやることがあった。
Pivotal Contribute License
Pivotal Contribute License へAgreement する必要があった。そのためGithubアカウントとリンクさせて、いくつかの会社の情報等を入力する。すると、以下のようにCIが進んでいく。
Reviewer Check
Reviewer がアサインされ、適切なLabelを付与してくれる。そして指摘事項がある場合はコメントが来る。自分の場合は、適切なCommit Messageじゃなかったようだ。Issue とのリンクをするためにCommit Messageを変更してほしいとのこと。
fixes gh-XXX
とすると PR Close 時にちゃんとIssue側もCloseしてくれるらしい。知らなかった
修正したものが問題ないと、Mergeされる
感想
とてもとても小さい修正だったが、初めてOSSへPRを出すことができた。それなりにハードルはあるが「Thanks」と言われ、無事Mergeされた時はうれしかった。これをきっかけに少しずついろんなOSSに積極的にPull Requestを送れるようになれば良いな。
なかなか自分一人ではやろうとすら思わなかったので、背中を押してくれた友人に感謝だ(本人はあんまりそんなつもりはなかったかもしれないが)