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

Krzysztof Kotowicz
Krzysztof Kotowicz

対応ブラウザ

  • 83
  • 83
  • x
  • x

ソース

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

DOM XSS はウェブ セキュリティの最も一般的な脆弱性の 1 つであり、開発チームが誤ってアプリに導入してしまうことがよくあります。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 シンクを使用できなくなります。

データが安全に処理されたことを示すために、Trusted Type という特別なオブジェクトを作成します。

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

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

Trusted Types の使用方法

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

オープンソースの go-csp-collector などのレポート コレクタをデプロイすることも、商用の同等のツールを使用することもできます。ブラウザで違反をデバッグすることもできます。

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

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

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

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 違反を修正する方法は 2 つあります。不適切なコードの削除ライブラリの使用Trusted Type ポリシーの作成が可能です。また、最後の手段としてデフォルト ポリシーを作成することもできます。

不適切なコードを書き直す

非準拠のコードが不要になったり、違反を引き起こす関数なしで書き換えられたりする可能性があります。

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

ライブラリを使用する

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

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

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

Trusted Type ポリシーを作成する

違反の原因となっているコードを削除できない場合や、値をサニタイズして 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 onerror=alert(1)>');
console.log(escaped instanceof TrustedHTML);  // true
el.innerHTML = escaped;  // '&lt;img src=x onerror=alert(1)>'

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

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 脆弱性を発生させる可能性があるのはポリシー内のコードだけです。ポリシーの作成を制限することで、さらに制限できます。

関連情報