com4dc’s blog

Javaプログラマーのはずだけど運用してます

Spring Securityのドキュメントを1から斜め読む 3

細かいヘッダとかの話。

細かすぎて知らないものが結構あったのと、多分デフォルトで有効になっているため意識して認識できてるものが少ない。

Referrer Policy

  • Referrerポリシーは、Webアプリケーションが活用することができる。ユーザーが最後に訪れたページを含むReferrerフィールドを管理するために。
  • Spring Securityのアプローチは Referrerポリシーヘッダを使用しており、異なるポリシーを提供する
    • どういうことだ??
Referrer-Policy: same-origin
  • Referrerポリシーレスポンスヘッダは、ユーザーが以前どこにいたかのソースを宛先に知らせるようにブラウザに指示する

Feature Policy

  • Feature Policy はWeb開発者が特定の API やWeb機能の動作を明示的に有効化、無効化、変更できるようにするためのメカニズムである
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

  • クライアントがHTTPを利用した時、Spring Security はHTTPSへリダイレクトするように構成することができる。ServletもWebFlux環境のどちらでも。

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 プロパティを使うことでアプリケーションの適切な構成を使用できる

次からようやく具体的なモジュールとかの話

OAuth 徹底入門の演習を解いてく第4章

4章はアクセストークンをどのように使うか。リソースサーバーを実装しながら役割と使い方を見る。

4 シンプルな OAuth の保護対象リソースの構築

4.1 OAuth トークンの解析

  • AccessToken を Request に付与する方法は3つ
    • Authorization ヘッダに Bearer トークンとして付与
    • encoded_form で Body にアクセストークンを付与
    • URL クエリパラメータとしてアクセストークンを付与
  • 推奨は 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.jsscope から対象を削除すれば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 異なるユーザーによる異なるデータの取得

  • 同じエンドポイントにアクセスするが、異なるユーザーでアクセスした場合、取得できるデータが異なる
  • サンプルの写真サービスがその例
    • ユーザーA がサードパーティアプリを認可し、写真サービスへのアクセスを許可した場合に取得できる写真
    • ユーザーB がサードパーティアプリを認可し、写真サービスへのアクセスを許可した場合に取得できる写真
    • 上記は異なるデータとなる
    • ただし、アクセスする写真サービスのエンドポイントはユーザー毎に変わらない(同じエンドポイント)

4.4 まとめ

  • アクセストークンの取得
  • アクセストークンの検証
  • scopeによる権限管理
  • アクセストークン取得後、実際にそのアクセストークンをどのように使って保護リソースにアクセスさせるかの実装。知識としてはあったけど、実装すると色々面倒だなぁと思った次第。

OAuth 徹底入門の演習を解いてく第3章

だからあれほど並行してやるなというのに・・・なぜ私はこう並行して手を出してしまうのか。

Spring Security のドキュメントを読む方で疲弊してくると今度こちらをやっている。

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.jsapp.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.jsprotectedResource 変数がリソースサーバーの場所。リソースサーバーはポート番号 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 したらどうするか?

  • ユーザーに再度トークンを取得してもらう
  • Refresh Token で自動的に新しいトークンを取得

次は 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_typerefresh_token を指定して認可サーバーの Token エンドポイントへトークン発行をリクエストする。

トークンレスポンスが返ってきたら Body を解析して access_tokenrefresh_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

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

Strict-Transport-Security は HTTPS リクエストのときだけ追加される

  • もしデフォルトで付与されるヘッダで不要なものがあれば、修正、削除が容易であり、これらのデフォルトに加えて追加することもできる
  • それぞれのヘッダの追加詳細の説明については以下の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 と翻訳すべきということを知ることができる
  • これにより中間者攻撃を大幅に制限することができる
  • RFC6797に従って、HSTSヘッダは HTTPSレスポンスにのみ注入される
  • ブラウザがヘッダを認識するためには、ブラウザははじめにSSL証明書に署名したCAを信頼しなければならない(SSL証明書だけではない)
  • サイトが 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攻撃が検出されたときに何をすべきかをブラウザに指示する
    • 例えば、フィルタはすべてをレンダリングするために最も危険性の低い方法に変更するかもしれない
    • 時にはこのようなコンテンツの変更がXSS脆弱性を新たに生む可能性がある。そのため、より良いのは修正するよりもブロックしてしまうことである
    • Spring Securityのデフォルトはこのヘッダが利用されており、コンテンツをブロックする
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/

感想

見たことがあるものもあれば知らないヘッダもあった。実際攻撃の仕方や、どんな攻撃に対して防御しているのかを知らずになんとなくつけていると何も知らないのと同等なので、ちゃんと一度頭を通しておくのは良さそう。

Recommend しないレスポンスヘッダについては広くサポートのためにあるけど、今だとあんまり採用されないのかな、くらいで理解した。

つぎは、PolicyまわりとHTTPまわり

Spring Securityのドキュメントを1から斜め読む

諸事情あってSpring Securityをちゃんと理解しないとダメになってしまったので、ひとまずドキュメントを全部読むから始める。

とはいえめちゃくちゃボリュームあるので適宜スキップしつつ、適当な翻訳と自分の理解のためだけにメモっていく。翻訳に関しては一応自分で解釈しつつ翻訳しながら、確認とちょっとした間違いを直したり確認したりするために、Google翻訳とDeepLを併用している。つまりわたしの翻訳がメインなので適当である。あまり信用あるソースにはならないということだけ書いておきます(こんなの参考にする人いないと思うけど)

実際日本語のプロジェクトはあるので、ちゃんとした翻訳はこちらを参考にしたほうが良いと思う。

spring.pleiades.io

日本語になっているとどうしても読み飛ばしてしまう癖やなんとなくで進んでしまうくせがあるので、わざと自分で理解しながらやらないとちゃんと理解できない。

読んでいくドキュメント

docs.spring.io

Part.1

Preface(序文)にあたるところ。基礎的なところについて諸々記載されている。

Authentication Support

  • Spring Securityはユーザー認証をサポートしている。各スタックでサポートされている内容の詳細はServletとWebFlux の認証のセクションを参照すること
    • servletとwebfluxで大きく異なるため、セクション自体が別に設定されてる

Password Storage

https://docs.spring.io/spring-security/site/docs/current/reference/html5/#authentication-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では以下の二つを提供
    • Synchronized Token Pattern: ページを返す際に hidden にランダムなIDを降るのでそれが合致しないと拒否る
    • SameSite Cookie: Cookieの属性にSameSiteをつける
      • Strict: めちゃくちゃ厳しいポリシー。絶対に同じドメインじゃないとCookieを送らない
      • Lax: ちょっとゆるふわ
      • SameSiteはブラウザが対応してないとダメ。古いやつだと対応してない可能性があるので注意
  • Webブラウザを使ってユーザーが操作する場合は原則CSRFを有効にする
  • ブラウザを利用しない場合は無効にする
  • Content-Typeを明確に指定しないことで、CSRF脆弱性が生まれる可能性があるので注意

実装の考慮事項

  • ログインリクエスト、ログアウトリクエストはセッションタイムアウトとの問題で悪意あるユーザーにログイン状態を乗っ取られて機密情報を見られる可能性がある(?)
  • CSRFとセッションタイムアウト
  • Multipart (File Upload)
    • CSRFトークンを確認するためにBodyを読もうとすると、ファイルのアップロードが始まってしまう
    • つまりCSRFトークンを読もうとして、別サイトからのアップロードを許可してしまう
      • 鶏が先か卵が先か
    • 解決策は二つ
      • BodyにCSRFトークンを配置して、一時ファイルとしてアップロードを許可
        • 割と一般的
      • URLに含める
        • ただしこれはリークする可能性があるので、ヘッダとかのメタ情報に入れた方がよい
    • HiddenHttpMethodFilter
      • いくつかのアプリケーションでは、フォームパラメータを利用してHTTPメソッドをオーバーライドできる。
      • 例えば以下のフォームを利用して、HTTPメソッドを投稿ではなく削除として使える
<form action="/process"
    method="post">
    <!-- ... -->
    <input type="hidden"
        name="_method"
        value="delete"/>
</form>
  • メソッドのオーバーライドは Filter で発生する。そのFilter は SpringSecurityサポートの前に設定しなければならない。オーバーライドは post に対してのみ発生するため、実際に問題になる可能性は低い。
  • しかし、Spring Securityの Filter 前に配置するのが最も良い方法である

とりあえずここまで。次はセキュリティヘッダについて

美味しい豚丼のタレ

北海道は豚肉がやたらめったら安い。

東京住んでいた頃は安いお得なお肉というと海外産でよくハナマサで大量購入して冷凍保存していたのだが、鶏肉にしろ豚肉にしろわざわざ海外産を選ぶ必要もなく、かなり安い値段で購入できるので、もっぱらお肉料理というと豚か鳥になりがちである。

牛肉については、北海道では牛肉をあまり食べる習慣が元々あまりないらしく値段は割と普通である(ただし北見を除く)

そのため、牛肉食べるときはオージービーフアメリカ産、もしくはふるさと納税で頂いたものが多い。

そもそも北海道に来るまで全く知らなかったのだが、こちらの人はすき焼きというと豚肉らしい。肉じゃがも牛肉ではなく豚肉らしい。ふーむ、方言等で困らない代わりに、こういった食生活の微妙な習慣の差はたまに出る。まあ、でも都内にいたころも牛肉は高級品だったのでそんな毎日食べれたわけではない。

で、北海道というと帯広発祥の豚丼である。単にタレをつけて豚肉を焼くだけなのだが、まあ普通に想像する通り美味い。これを自作したのでメモる。

レシピ

豚丼のタレは結構いろいろなところで売っている有名なところはソラチのタレだろうか。何故か二種類あるのだが、材料も何もかも同じで何が違うのかわからない。多分道民でもわからないと思う。

そういったものを使うのも良いのだが買い物にでるのも面倒だったので今回は自作した。

オリジナルレシピはこちら

oceans-nadia.com

これを少しアレンジした。

材料(二人分)

  • 豚肉
    • 今回は焼肉用にカットされていてそれなりに分厚いバラっぽいところ。美味い
  • 白髪ねぎ
    • ないよりあったほうが良い。美味い
  • タレ
    • 砂糖、みりん、醤油、酒:おおさじ2
    • 生姜チューブ、にんにくチューブ:4cmくらい
    • 生姜刻み:一かけくらい
    • 水:80cc

手順

オリジナルレシピは材料の量くらいでほかはほぼ無視してしまった(間違えた

  1. タレの材料をフライパンに入れて沸々と煮る
  2. 少しとろみが出てくるくらいで1/4くらいをフライパンに残して、残りは耐熱容器にとっておく
  3. 少なめのタレで豚肉を絡め焼きにする。すこし醤油が焦げるくらいのほうが豚肉に焦げっぽい味がついて美味しい。そのままだとどんどん焦げていくので豚肉でタレを拭き取りながら焼く感じ
  4. 火が通ったらほかほかご飯の上にオンザライス
  5. タレをフライパンに戻してもう少しとろみがつくまで水分を飛ばす
  6. 好みのとろみになったら、ご飯の上の豚肉にかけて白髪ねぎを散らす

感想

うまい。無限にご飯が食える。

Spring Securityに微修正Pull Requestを初めて送った

普段業務ではSpring BootとSpring Securityをメインで実装に利用している。 現在使っているSpring Securityのバージョンが2系で古く、Spring Security 5を調査する必要があった。

そもそも Spring Security 5.x系自体出てすでに数年経過しているので、それなりに日本語の情報もある。のだが、微妙にバージョンによってプロパティ名が違ったりして都度試行錯誤しながら検証している。

そんな中公式のリファレンスを参照していたところ、間違いではないのだが他のプロパティ名とフォーマットが違うものが一つだけあるのに気がついた。

docs.spring.io

spring.security.oauth2.client.provider.[providerId].user-name-attribute

すでにバックポート修正済みだが、この箇所が元々 userNameAttribute になっていた。

というのをTweetしたところ「コントリビュートチャンス!」と言われたので素直に従ってみた。

Spring Security

Githubリポジトリは以下。

github.com

該当のファイルは以下のパスにあった。

  • docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-login.adoc

adoc という形式を初めてみたのだが、 AsciiDoc というものらしい。

AsciiDoc - Wikipedia

該当箇所を修正する。

まずは Issue をたてる

小さいとはいえ、リリースされているドキュメントを修正するのだから Issue は立てておくべきだろう、と思って Issue を立てた。他のOSSとは違ってあまり厳格な Issue のガイドラインがないようだ。 ISSUE TEMPLATE と他の Issue を参考にして修正Issueをあげた

userNameAttribute case style is different others · Issue #8169 · spring-projects/spring-security · GitHub

とても適当な英語で恐縮なのだが、内容は伝わったらしい。 Label が自動的につけられ Triage 待ちになった。とりあえずやってみるか精神でやっていたので、この時点でPull Request の作成も開始した。

Repository を Fork する

どうやら他のPull Requestを見ると、リポジトリを Fork して修正した内容を本流へ Pull Request として作成するのが正しい手順のようだ。このタイプの作業は始めてだったので、Github公式のドキュメントを参照した。

フォークからのプルリクエストの作成 - GitHub ヘルプ

手順に従って以下を実施した。

  1. Repository を Fork して自身のアカウント配下に置く
  2. fix ブランチを作成する。
    • gh-8169-fix-property-case-style こんな名前のブランチで作った。Issue 番号と概要。
    • こちらも色々探したものの厳格なガイドラインがなかったので見様見真似
  3. 修正する
  4. fix ブランチを Push
  5. 一応最新になっているかを確認するために、本流のmasterを取り込んでrebaseする。
  6. spring-projects:master に向けて Pull Request を作成する

やったことはこれだけ

Fix userNameAttribute property case style by komuro-hiraku · Pull Request #8171 · spring-projects/spring-security · GitHub

PR 作成後の手順

その後いくつかやることがあった。

Pivotal Contribute License

Pivotal Contribute License へAgreement する必要があった。そのためGithubアカウントとリンクさせて、いくつかの会社の情報等を入力する。すると、以下のようにCIが進んでいく。

f:id:com4dc:20200409000839j:plain

Reviewer Check

Reviewer がアサインされ、適切なLabelを付与してくれる。そして指摘事項がある場合はコメントが来る。自分の場合は、適切なCommit Messageじゃなかったようだ。Issue とのリンクをするためにCommit Messageを変更してほしいとのこと。

fixes gh-XXX とすると PR Close 時にちゃんとIssue側もCloseしてくれるらしい。知らなかった

修正したものが問題ないと、Mergeされる

f:id:com4dc:20200409001314j:plain

感想

とてもとても小さい修正だったが、初めてOSSへPRを出すことができた。それなりにハードルはあるが「Thanks」と言われ、無事Mergeされた時はうれしかった。これをきっかけに少しずついろんなOSSに積極的にPull Requestを送れるようになれば良いな。

なかなか自分一人ではやろうとすら思わなかったので、背中を押してくれた友人に感謝だ(本人はあんまりそんなつもりはなかったかもしれないが)