Trusted Types で DOM ベースのクロスサイト スクリプティングの脆弱性を防止

Krzysztof Kotowicz
Krzysztof Kotowicz

対応ブラウザ

  • 83
  • 83
  • x
  • x

ソース

DOM ベースのクロスサイト スクリプティング(DOM XSS)は、ユーザーが制御するソース(ユーザー名、URL フラグメントから取得したリダイレクト URL など)からデータがシンク(eval() などの関数または .innerHTML などのプロパティ セッター)に到達したときに発生します。

DOM XSS は最もよくあるウェブ セキュリティの脆弱性の一つであり、開発チームがアプリに誤って DOM XSS を導入してしまうことがよくあります。Trusted Types は、危険なウェブ API 関数をデフォルトで保護することで、アプリケーションの作成、セキュリティ レビュー、DOM XSS の脆弱性の排除を行うためのツールを提供します。Trusted Types は、ブラウザがまだサポートしていない場合にpolyfillとして使用できます。

背景情報

長年にわたり、DOM XSS はウェブ セキュリティの脆弱性で最も広範で危険な脆弱性の一つです。

クロスサイト スクリプティングには 2 種類あります。一部の XSS 脆弱性は、ウェブサイトを構成する HTML コードを安全に作成するサーバーサイド コードが原因で発生します。また、クライアントで根本原因があり、JavaScript コードがユーザー制御のコンテンツを使用して危険な関数を呼び出す場合もあります。

サーバーサイドの XSS を防止するため、文字列を連結して HTML を生成しないでください。バグの軽減策として、安全なコンテキスト自動エスケープ テンプレート ライブラリとノンスベースのコンテンツ セキュリティ ポリシーを使用してください。

また、ブラウザで Trusted Types を使用することで、クライアント側の DOM ベースの XSS を防ぐことができます。

API の概要

Trusted Types は、リスクの高い次のシンク関数をロックダウンすることで機能します。ブラウザ ベンダーやウェブ フレームワークによっては、セキュリティ上の理由からこれらの機能の使用を控えるものもあるため、すでにご存じかもしれません。

Trusted Types では、データをシンク関数に渡す前に処理する必要があります。ブラウザはデータが信頼できるかどうかを判断できないため、文字列のみを使用すると失敗します。

すべきでないこと
anElement.innerHTML  = location.href;
Trusted Types を有効にすると、ブラウザは TypeError をスローし、文字列で DOM XSS シンクを使用できなくなります。

データが安全に処理されたことを示すために、信頼できるタイプという特殊なオブジェクトを作成します。

推奨事項
anElement.innerHTML = aTrustedHTML;
  
Trusted Types が有効になっている場合、ブラウザは HTML スニペットを想定しているシンクの TrustedHTML オブジェクトを受け入れます。他の機密性の高いシンクには、TrustedScript オブジェクトと TrustedScriptURL オブジェクトもあります。

Trusted Types により、アプリケーションの DOM XSS 攻撃対象領域が大幅に減少します。これにより、セキュリティ レビューが簡素化され、実行時にブラウザでコードをコンパイル、lint チェック、またはバンドルする際にタイプベースのセキュリティ チェックを適用できます。

Trusted Types の使用方法

コンテンツ セキュリティ ポリシー違反の報告に備える

オープンソースの reporting-api-processorgo-csp-collector などのレポート コレクタをデプロイするか、商用の同等のものを使用できます。ReportingObserver を使ってブラウザでカスタム ロギングを追加し、違反をデバッグすることもできます。

const observer = new ReportingObserver((reports, observer) => {
    for (const report of reports) {
        if (report.type !== 'csp-violation' ||
            report.body.effectiveDirective !== 'require-trusted-types-for') {
            continue;
        }

        const violation = report.body;
        console.log('Trusted Types Violation:', violation);

        // ... (rest of your logging and reporting logic)
    }
}, { buffered: true });

observer.observe();

または、イベント リスナーを追加します。

document.addEventListener('securitypolicyviolation',
    console.error.bind(console));

レポート専用の CSP ヘッダーを追加する

Trusted Types に移行するドキュメントに、次の HTTP レスポンス ヘッダーを追加します。

Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; report-uri //my-csp-endpoint.example

現在、すべての違反が //my-csp-endpoint.example に報告されていますが、ウェブサイトは引き続き機能しています。次のセクションでは、//my-csp-endpoint.example の仕組みについて説明します。

Trusted Types 違反の特定

今後は、Trusted Types が違反を検出するたびに、構成済みの report-uri にレポートを送信します。たとえば、アプリケーションが innerHTML に文字列を渡すと、ブラウザは次のレポートを送信します。

{
"csp-report": {
    "document-uri": "https://my.url.example",
    "violated-directive": "require-trusted-types-for",
    "disposition": "report",
    "blocked-uri": "trusted-types-sink",
    "line-number": 39,
    "column-number": 12,
    "source-file": "https://my.url.example/script.js",
    "status-code": 0,
    "script-sample": "Element innerHTML <img src=x"
}
}

これは、39 行目の https://my.url.example/script.js で、<img src=x で始まる文字列で innerHTML が呼び出されたことを示しています。この情報は、コードのどの部分に DOM XSS が引き起こされ、変更が必要なのかを絞り込むのに役立ちます。

違反を修正する

信頼できるタイプの違反を修正するには、いくつかの方法があります。問題のあるコードを削除し、ライブラリを使用して、Trusted Type ポリシーを作成できます。また、最後の手段としてデフォルト ポリシーを作成することもできます。

問題のあるコードの書き換え

準拠していないコードは、不要になったり、違反の原因となる関数なしで書き換えられたりする可能性があります。

推奨事項
el.textContent = '';
const img = document.createElement('img');
img.src = 'xyz.jpg';
el.appendChild(img);
すべきでないこと
el.innerHTML = '<img src=xyz.jpg>';

ライブラリを使用する

一部のライブラリでは、シンク関数に渡すことができる Trusted Types がすでに生成されています。たとえば、DOMPurify を使用して HTML スニペットをサニタイズし、XSS ペイロードを削除できます。

import DOMPurify from 'dompurify';
el.innerHTML = DOMPurify.sanitize(html, {RETURN_TRUSTED_TYPE: true});

DOMPurify は Trusted Types をサポートしており、ブラウザが違反を生成しないように、サニタイズした HTML を TrustedHTML オブジェクトにラップして返します。

Trusted Type ポリシーを作成する

違反の原因となっているコードを削除できず、値をサニタイズして信頼できるタイプを作成するライブラリが存在しないことがあります。そのような場合は、Trusted Type オブジェクトを自分で作成できます。

まず、ポリシーを作成します。ポリシーは、Trusted Types のファクトリであり、入力に特定のセキュリティ ルールを適用します。

if (window.trustedTypes && trustedTypes.createPolicy) { // Feature testing
  const escapeHTMLPolicy = trustedTypes.createPolicy('myEscapePolicy', {
    createHTML: string => string.replace(/\</g, '&lt;')
  });
}

このコードは、createHTML() 関数を使用して TrustedHTML オブジェクトを生成できる myEscapePolicy というポリシーを作成します。定義済みのルールでは、< 文字を HTML エスケープして、新しい HTML 要素が作成されないようにします。

次のようなポリシーを使用します。

const escaped = escapeHTMLPolicy.createHTML('<img src=x 
console.log(escaped instanceof TrustedHTML);  // true
el.innerHTML = escaped;  // '&lt;img src=x 

デフォルト ポリシーを使用する

CDN からサードパーティ ライブラリを読み込む場合など、問題のあるコードを変更できないことがあります。その場合は、デフォルト ポリシーを使用します。

if (window.trustedTypes && trustedTypes.createPolicy) { // Feature testing
  trustedTypes.createPolicy('default', {
    createHTML: (string, sink) => DOMPurify.sanitize(string, {RETURN_TRUSTED_TYPE: true})
  });
}

default という名前のポリシーは、Trusted Type のみを受け入れるシンクで文字列が使用されている場合に使用されます。

コンテンツ セキュリティ ポリシーの適用に切り替える

アプリケーションで違反が発生しなくなったら、Trusted Types の適用を開始できます。

Content-Security-Policy: require-trusted-types-for 'script'; report-uri //my-csp-endpoint.example

ウェブ アプリケーションがどれほど複雑であっても、DOM XSS の脆弱性をもたらす可能性があるのは、いずれかのポリシー内のコードだけです。ポリシーの作成を制限することで、その制限をさらに緩和できます。

参考資料