🔏

Trusted Types API とはなんぞや

2024/10/26に公開

結論

DOM XSS攻撃を防ぐためのWeb標準APIです。
(利用できるブラウザは限られています。)

参考

https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API

サンプルアプリ

DOM XSS対策をしているフォーム、していないフォームの比較サンプル
https://stackblitz.com/edit/vitejs-vite-sx1vsg?embed=1&file=index.html

CSPとTrusted Types APIを連携したサンプル
https://stackblitz.com/~/github.com/atlansien/dom-xss-guard-trusted-types-and-csp?file=.git/COMMIT_EDITMSG&file=index.html

前置き

先日のVue Fesで以下のような発表がありました。

https://speakerdeck.com/lycorptech_jp/20241024a

(Vue3.5からですが)こちらを利用することでv-htmlを利用してもDOM XSS攻撃を防げるということです、素敵ですね。
ただ、そもそもTrusted Types APIというものの存在自体を僕は知りませんでした。なのでこれを機に調べてみつつ、サンプルアプリを作ってみました。

XSS攻撃とは?

簡単に言うとユーザーがクライアント側から悪意のあるコード・スクリプトをWebサイト内に埋め込む手法です。サーバーサイド、クライアントサイド両方で脅威となる場合があります。

参考: MDN Docs: クロスサイトスクリプティング (XSS)

今回は、クライアントサイド側のXSS(DOM XSS)についてのお話です。ここからはこちらのサンプルアプリを利用しつつ説明したいと思います。よければ皆さんも一緒に触ってみてください。
https://stackblitz.com/edit/vitejs-vite-sx1vsg?embed=1&file=index.html

説明

まずはDOM XSSがどういうものか実際に試してみましょう。
安全ではない実装のinputにサンプルコードを入力、そして表示ボタンをクリックします。(ここでは2.のコードを利用しました)

出力された文字の上にカーソルを移動すると、アラートが表示されまます。このように、ユーザーが任意のスクリプトを実行することができてしまいました。
ただのアラートであれば可愛いものですが、これが情報を漏洩させるようなコードだったら、、、?怖いですね。

もちろんですが、Zennではきちんと対策されています。
<div onmouseover='alert(2)'>マウスを上に置いてください</div>

現状ではエスケープ処理やサニタイズ処理を行いコードを無害化して出力する方法や CSPと呼ばれるHTTPヘッダーでスクリプトの実行を制限する設定を用いる方法があります。

以下は replaceを用いてコードをエスケープ処理したサンプルになります。

HTMLタグも含め、入力した値が丸ごと表示されています。文字の上にカーソルを置いてもアラートは表示されません。コンソールログを見るとエンティティに変換することで無害化されています。

window.displayReplaced = function () {
  const input = document.getElementById('replaceInput').value;
  const replaced = input.replace(
    /[&<>""']/g,
    (match) =>
      ({
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#39;',
      }[match])
  );
  console.log('replaced: ', replaced);
  document.getElementById('replaceOutput').innerHTML = replaced;
};

そして、新たなDOM XSS攻撃対策の手段として実装されたものがTrusted Types APIになります。

Trusted Types API とは

特定の型に基づいたオブジェクトに変換するポリシーを作成して、innerHTMLなどのDOMに安全なHTMLを挿入するためのAPIです。

以下はtrusted Types APIを用いた二つの例です。

上は入力された値をエスケープ処理するポリシーです。ログを見るとTrustedHTMLオブジェクトになっていることがわかります。

window.displayUseTrustedTypes = function () {
  const input = document.getElementById('trustedInput').value;
  const escapeHTMLPolicy = trustedTypes.createPolicy('myEscapePolicy', {
    createHTML: (string) =>
      string
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&apos;'),
  });
  const escaped = escapeHTMLPolicy.createHTML(input);
  console.log('trustedtypes', escaped);
  document.getElementById('trustedOutput').innerHTML = escaped;
};

下は入力した値ではなく、別の文字が出力されていますね。こちらはXSSの可能性がある文字パターンの場合、エラーを出力するポリシーにしています。エラーなのでログには何も表示されません。

window.displayUseTrustedTypesError = function () {
  const input = document.getElementById('trustedErrorInput').value;
  const escapeHTMLPolicy = trustedTypes.createPolicy('myErrorPolicy', {
    createHTML: (string) => {
      // XSSの可能性がある文字列パターンをチェック
      const xssPatterns = [
        /<script\b[^>]*>/i,
        /javascript:/i,
        /data:/i,
        /on\w+\s*=/i,
        /style\s*=/i,
      ];

      // XSSパターンが検出された場合はエラーを投げる
      for (const pattern of xssPatterns) {
        if (pattern.test(string)) {
          throw new Error('潜在的なXSSが検出されました: ' + string);
        }
      }

      // 安全な文字列のみを許可
      const div = document.createElement('div');
      div.textContent = string;
      return div.innerHTML;
    },
  });
  try {
    const escaped = escapeHTMLPolicy.createHTML(input);
    console.log('trustedtypesError', escaped);
    document.getElementById('trustedErrorOutput').innerHTML = escaped;
  } catch (e) {
    document.getElementById('trustedErrorOutput').textContent =
      '⚠️ エラー: ' + e.message;
  }
};

他にも、CSPと併用してさらに強固な実装を行うこともできます。
Web.dev: Trusted Types で DOM ベースのクロスサイト スクリプティングの脆弱性を防止

今回のような簡単なサンプルの場合では、replaceでも十分な対策はできます。しかしより巨大な構造になってくると、CSPと併用ができたり、TrustedHTML型など特別な型でのチェックができるTrusted Types APIが有用になってきます。

というわけで、せっかくなのでCSPをヘッダーに設定してTrusted Types APIと連携させた別のサンプルアプリを用意しました。
https://stackblitz.com/~/github.com/atlansien/dom-xss-guard-trusted-types-and-csp?file=.git/COMMIT_EDITMSG&file=index.html

Trusted Types API & CSP

上がcreatePolicyを利用したInput、下が利用していないInputです。

画面を見ると、上は値が表示されていますが、下は意図しない文字が表示されています。コンソールログを見るとエラーが出力されていますね。

TypeError: Failed to set the 'innerHTML' property on 'Element': This document requires 'TrustedHTML' assignment.
    at HTMLButtonElement.<anonymous> (main.js:68:27)

このようにCSPとTrusted Types API を利用することで、XSS DOM攻撃に対する対策漏れがない設計にすることができます。

利用上の注意

ただ、こちらまだ新しい技術ということで、残念ながら利用できるブラウザが限られてしまいます。
2024/10/25現在では以下のような対応表となっています。


こちらはSafariですが、エラーになります。

また、Trusted Types APIには他にもさまざまな機能やユースケース・制約があります。今回は書ききれなかったので、詳しくはMDNなどを見て調べてみてください。

終わりに

ということで、Vue.jsでもTrusted Types APIに対応することで、さらにセキュリティ対策の幅が広がるということです。いいことですね。v-htmlはXSSの原因になるから絶対に使ってはいけない。と口を酸っぱく言われるのがいずれ懐かしくなる日が来そうです。

Discussion