こんにちは kotamat です。 書いていたらほとんどVue.jsの話じゃなくなっちゃいましたが、SPAの構成において気をつけるセキュリティーに関して紹介したいと思います。

免責

もちろんすべての脆弱性を網羅しているわけではないため、ここに紹介しているものをすべてやれば完全にセキュアというわけではありません。 簡易にTIPSを紹介するという目的となります。

DOM Based XSS

Vue.jsなどDOMを生成するフロントエンドのフレームワークを使うにあたって切っても切り離せない脆弱性の一つがこれでしょう。参考URL JavaScriptによって生成されるHTML DOMによって攻撃者が任意の実行されるJavaScriptコードを注入できるようになる脆弱性であり、攻撃を受けた被害者のブラウザで実行されてしまう事により、機微な情報の漏えい等が発生してしまう可能性のある問題をはらんでいます。

Vue.jsではmustaches記法{{}}をテンプレート内部に用いることによって、DOM Based XSSの原因となる文字列をエスケープした状態で出力することができます。

ただ、これを行うと、通常のhtmlタグもエスケープされて出力されてしまうため、CMSなどのコンテンツ内部にhtmlタグを仕込むようなアプリケーションではそのままでは使うことはできません。

これを回避するためにv-htmlというエスケープなしでコンテンツを描画する機能がVue.jsにはあります。もちろんこのまま使うと上記脆弱性の可能性が非常に高くなるため、適切なエスケープ処理を施す必要があります。

また、これだけではなく、aタグのhref属性も攻撃対象となります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<template>
<a href="url">...</a>
</template>
<script>
export default {
    data:{
        url: "javascript:alert(1)"
    }
}
</script>

といったようなhrefの指定の場合、aタグをクリックすることによってjavascriptが実行できてしまいます。 これを回避するためにはhttp, httpsから始まる文字列ないしは、相対パスで始まるリンクはそれが正しく遷移できるものであるかどうかをチェックしなければなりません。

// 細かい説明は第5回 問題を発生させにくくするURLの扱い方を参考に。

ただ、上記をいくら頑張ってプロダクトのコードを100%XSS安全にしたとしても、npmの外部パッケージが参照しているコードがXSS脆弱な場合、防ぐことはできません。(特にUIフレームワークを使っている場合) 故にXSS脆弱だとしても、最低限漏れてはならない情報がもれないようにする必要があります。

認証トークンの保持方法

Vue.jsを使うとなった場合、APIとの通信を行うようなアプリケーションを用いることが多いかと思います。 そういった場合、JWT形式などのaccess tokenを認証後にAPIから取得したものをどこに保管すべきでしょうか。

localStorageやsessionStorageには保管しないというのがベストプラクティスとして語られています。 Don’t store tokens in local storage - Auth0 Store JWTs securely - JWT authentication: When and how to use it

理由としては、上記XSSを100%回避できないという問題が起因しています。XSSを100%回避できない以上、APIサーバー側で認証を弾けるように実装しておく必要があります。 それを防ぐためにはhttpOnlyフラグの付いたCookieに保存するオンメモリ(=Vuexなどのグローバルコンテキスト内)のみにTokenを保持する方法が推奨されています。

後者を行う場合、ブラウザのリロードを行うだけで再度ログインをしなければならず、UXはとても悪くなってしまいます。 これを防ぐためにAuth0では、Silent Authenticationと呼ばれる機構が存在します。 ※IDaaSの説明は省く これはAuth0.jsという公式が提供しているSDKを用いることによって実現できるのですが、透過のiframeを作成し、Auth0側ですでに認証されているセッション中はrenew authすることによってaccess tokenを再発行し、それをpostMessageでフロントエンドに返すことによってあたかもユーザはtoken再発行してなかったかのようにプロダクトを触り続けることができるというものです。 iframeでの処理なので、SPA構成でも構築可能なものではありますが、IDaaSとしてAuth0にロックインされることが懸念事項でしょう。 これを自社プロダクトで実行する場合は同様にiframeを生成するAPIをAPI側に用意し、セッションデータをiframe内のhttpOnlyクッキーに保存し、それを元に都度access tokenを取得した上でフロントエンドにpostMessageし、かつオンメモリでトークンを扱うことによってセキュアに実装することができます。

iframeによる脆弱性

ここでiframeについての脆弱性を紹介します。 iframeは他のサイトのコンテンツを表示できる機能でHTMLタグで実装できます。 ここで特に何も対策をしていないと、あらゆるサイトからiframeで表示されるため、悪意のあるサイトを経由した脆弱性を付かれる可能性があります。

  • postMessage送信によるデータ授受
  • クリックジャギング

postMessageによるデータ授受

前に紹介させてもらったトークンの受け渡しに際し、postMessageでiframeの親にデータを渡す方法を紹介させてもらいました。 もちろんこれを悪意のあるサイトが参照できてしまうと問題です。

これを防ぐために、postMessageでは第2引数にtargetOriginを設定することができます。 これはiframeの親のoriginを指定するものであり、*もしくは当該Originのどちらかを指定することができます。 基本的に*を実装してしまうと、どのサイトからでもメッセージを受信できてしまうため、許可するOriginのみを指定するようにしてください。

また、逆に呼び出しもとのiframeの親でもOriginのチェックが必要です。 呼び出したURLとは別のURLにリダイレクトしている可能性があるためです。

1
2
3
4
5
6
7
window.addEventListener("message", receiveMessage, false);

function receiveMessage(event) {
// Originのチェック
  if (event.origin !== "http://trusted.domain:8080")
    return;
}

このようにoriginを確認することによって、このメッセージがtrusted.domeinから来ていることを確認できます。

iframeの説明をしましたが、window.popupなどのポップアップでも使うことができます。

クリックジャギング

前述のpostMessageだけでは、表示されるコンテンツまでは制御できません。 iframeをそもそも表示する先をHTTPのレスポンスヘッダーを用いて制御することが可能です。

実現方法は現状2パターンが存在します。

旧ブラウザと書きましたが、基本的にX-Frame-Optionsは全ブラウザで対応しています。 ただし、複数ドメインから参照されることを許可するには(自己ドメインと他サイトの2つでも同様)ALLOW-FROMというパラメータを用いる必要があるのですが、Chrome, Safariなど、ユーザがよく使っているブラウザには対応していないため、複数サイトからの参照が必要になったタイミングで、変更を余儀なくされてしまいます。

一方CSPのframe-ancestorsでは複数のドメインを列挙することが可能です。こちらに対応していないブラウザはIEのみとなっているため、IEを除外したアプリケーションではこちらを使いましょう。

HSTS

HSTS(Hypertext Strict Transport Security)とは、TLSをブラウザに強制するポリシーメカニズムです。 TLSは通信暗号化のメカニズムですが、それをユーザ側に強制させることによって中間者攻撃を強制的にシャットアウトします。 前述のセキュリティ対策を行ったとしてもセキュアでない通信プロトコルで通信すればトークンは盗み見られてしまうため、この対策も検討しましょう。 有効にする場合HTTPのレスポンスヘッダーにStrict-Transport-Securityを指定します。

フラグには「max-age」「includeSubDomains」「preload」を指定することができます

  • max-age
    • ブラウザがHSTSを保持する期間を指定します。基本的には10886400秒(18週)以上を指定することを推奨していますが、間違いがあるとその間修正できないので、全ページでHTTPS通信ができることを確認した上で秒数を上げるようにしましょう
  • includeSubDomains
    • 対象をサブドメインにも波及させるかどうかを指定します。
  • preload
    • HSTS Preload Listに登録するかどうかを指定します。登録することで、初回アクセス時でもHSTSのレスポンスなしにHTTPS接続を強制することができます。

ちなみに執筆時点ではGCPのロードバランサーには実装されていないようです Allow setting HSTS Strict-Transport-Security header

静的ホスティングした場合の対処

Vue.jsを通常扱う場合、SSRで運用するか、ビルドされた静的ファイルをS3などの可用性の高いストレージに保管し、ホスティングするかと思います。

前者の場合は、今まで紹介したようなレスポンスヘッダーを初回アクセス時に返却することによって実現できますが、後者の場合はレスポンスヘッダーをいじることが難しいかと思います。

AWSではLambda Edgeという、CloudFrontから配信されるリクエストやレスポンスに追加の処理を書ける機能があります。 これを用いることによって、レスポンスヘッダーに今まで紹介したようなヘッダーを付けるようにします。

1Behaviorごとに1つのレスポンス用Lambda関数しかアタッチできないため、下記のように、すべての処理を一つの関数に収める必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
'use strict';

exports.handler = (event, context, callback) => {
    const response = event.Records[0].cf.response;
    const headers = response.headers;

    // HSTS
    headers['strict-transport-security'] = [{
        key: 'Strict-Transport-Security',
        value: 'max-age=63072000; preload'
    }];

    // clickjacking
    const allowHosts = [
        "'self'", // 自分のドメイン
        "trsuted.domain", // 信用できるドメイン
    ];
    headers['content-security-policy'] = [{
        key: 'Content-Security-Policy',
        value: 'frame-ancestors ' + allowHosts.join(" ")
    }];

    callback(null, response);
};

まとめ

Vue.jsで用いられる脆弱性を紹介させてもらいました。 この他にも脆弱性となるような処理があるかもしれません。 OWASPが提供しているTesting Guideを参考に、これ以外にも脆弱性がないか確認してみるのはいかがでしょうか?