Chrome_v83でサポートされたTrustedTypes: XSS対策

   · ☕ 6 min read

trustedTypes.jpg
Chrome の version83 より追加された TrustedTypesを使用した。その概要と大まかな使用方法、コードを記す。記載にあたっては、Prevent DOM-based cross-site scripting vulnerabilities with Trusted Typesを参照した。

TL;DL

下記のページに Trusted Types の動作確認コードをホストした。動作のイメージを掴みたい方は、アクセスして Console を確認いただくと理解が早いかもしれない。

Netlify: https://laughing-shirley-de1838.netlify.app/
ソースコード:https://github.com/benibana2001/til/tree/master/csp/dist

Trusted Types とは

概要

DOM ベースの XSS を回避するために導入されたブラウザ API。2020 年 6 月現在 Draft。

W3C の仕様: Trusted Types
GitHub: w3c/webappsec-trusted-types

DOM-based XSS?

DOM-based cross-site scripting とは何か。これは以下のように言われている。

DOM-based cross-site scripting happens when data from a user controlled source (like user name, or redirect URL taken from the URL fragment) reaches a sink, which is a function like eval() or a property setter like .innerHTML, that can execute arbitrary JavaScript code.

つまり、JavaScript の DOM 操作に関する一部の機能の穴を突き、XSS を行うこと。具体的には、DOM 情報を動的に操作する script、例えばinnerHTMLdocument.writeなどを使用した際に発生しうる。

Trusted Types によって実行できること

Trusted Typesを有効化しておくと、innerHTML等を使用した XSS の原因となりうるコードを記述した際、ブラウザ上で実行エラーとなる。これにより開発者は、事前に危険なコードを検知し、修正する機会を得ることができる。

1
2
3
4
// 事前にTrusted TypesをONにしておくと...
const elem = document.getElementById("myDiv");
// 下記のコードに危険が含まれることが、ブラウザ実行時に検出される
elem.innerHTML = `Hello, world!`;

Trusted Types は危険を知らせてくれる。しかしこの機能を使用したからといって、コードが安全になるわけではない。XSS を防ぐためには、開発者自身がコードを修正する必要がある。

例えば開発者がinnerHTMLを使用する際は、代わりに以下のような手段を検討すべきだ。

  • innerHTMLの代わりにtextContentを使用する
  • DOMPrifyのようなライブラリを使用しサニタイズを行う

これらの方法を使用してコードが安全な状態(trustedHTML)となった時、Trusted types はエラーを吐くのをやめる。

Trusted Types を使用する方法

ここでは Trusted Types を使用する方法について確認する。

使用準備

HTTP レスポンスのヘッダーに下記を設定することで、Trusted Types を有効化することができる。また URI を指定することで、エラーを通知することができる(要 HTTPS)。

Content-Security-Policy: require-trusted-types-for 'script'; report-uri

上記の記述は、コンテンツセキュリティポリシー (CSP)と呼ばれるセキュリティレイヤーの設定を行なっている。これはXSS をはじめとする脅威を検知するためのものだ。

上記設定をサーバーに対して行うことで、Trusted Typesは有効となる。しかし<meta>要素を用いて指定することも可能だ。その際は下記のような形となる。

1
2
3
4
5
<!-- MDN より引用-->
<meta
  http-equiv="Content-Security-Policy"
  content="require-trusted-types-for 'script';"
/>

ただし、Content-Security-Policy-Read-Onlyというエラー通知用のレイヤーに関してはmetaタグでの使用はサポートされていないため注意が必要だ。

Read-Onlyはmetaタグに埋込不可

CSP の詳細についてはMDN/コンテンツセキュリティポリシーがわかりやすい。

エラー内容

詳細について今回は触れないが、上記のContent-Security-Policy-Report-Onlyというエラー確認用のレイヤーを設定した場合は、違反が発生した際にレポートという形で、以下のような情報が送られる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{
  "csp-report": {
    // 設定したuri
    "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"
  }
}

Trusted Types を使ってポリシーを作成

違反が確認された際は、修正を行う必要がある。
上述したが、修正の方法は複数存在する。ただし場合によっては、サニタイズのためのライブラリが使用できないこともある。そんな時は、自前でポリシーの実装をする必要がある。

ポリシーとは、安全なコードを作るための機能だ。具体的にはwindow.trustedTypesオブジェクトのcreatePolicyメソッドを使用して作成する。

以下のコードではポリシーを作成し、そこに文字列をエスケープするための機能を追加している。

1
2
3
4
5
6
7
8
// Trusted Types の存在を確認
if (window.trustedTypes && trustedTypes.createPolicy) {
  // エスケープのためのポリシーを定義
  const escapeHTMLPolicy = trustedTypes.createPolicy("myEscapePolicy", {
    // ポリシーに持たせる関数を定義
    createHTML: (string) => string.replace(/\</g, "&lt;"),
  });
}

このポリシーを下記のように使用することで、文字列をエスケープできる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 危険な文字列を生成
const dangerString = "<img src=x onerror=alert(1)>";
// 作成したポリシーを使用して文字列をエスケープ
const escaped = escapeHTMLPolicy.createHTML(dangerString);

// エスケープされているか確認
console.log(escaped instanceof TrustedHTML); // true

// エスケープされた文字列を割り当て。エラーは起きない
el.innerHTML = escaped; // '&lt;img src=x onerror=alert(1)>'

実際に TrustedTypes を使用してみる

ここでは危険を含むコードを実際に Netlify にホストし、どのような結果になるのか確認する。

ホストするコードを用意

今回は、以下の2つのコードを用意して TrustedTypes の動きを確認する。

    1. 故意に脅威を含ませたコード
    1. ポリシーを作成し、1 に対して適用を行い、無害化したコード

はじめに 1 のコードを用意する。

1
2
3
4
5
6
// 脅威の発生時にエラーをConsoleに出力するようイベントを設ける
window.addEventListener("securitypolicyviolation", (err) => console.error(err));

const body = document.body;
// 故意に危険な動作を実行する。
body.innerHTML = "<img src=xyz.jpg>";

続いて 2 のコードを用意する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
window.addEventListener("securitypolicyviolation", (err) => console.error(err));

// TrustedTypesが使用できるか確認
if (window.trustedTypes && trustedTypes.createPolicy) {
  // ポリシーを作成
  const escapedHTMLPolicy = trustedTypes.createPolicy("myEscapePolicy", {
    createHTML: (string) => string.replace(/\</g, "&lt;"),
  });

  // 危険なコードを無害化
  const escaped = escapedHTMLPolicy.createHTML("<img src=xyz.jpg>");

  // 無害化されているか確認
  console.log(escaped instanceof TrustedHTML);
  const body = document.body;
  body.innerHTML = escaped;
}

上記 2 つのコードをホストする。

Netlify にホスト

Netlify を使用して、上記の 2 つのコードをhttps://laughing-shirley-de1838.netlify.app/にホストした。こちらのページで Console を確認すると下記のエラーが確認できる。

TrustedTypesを有効化した状態で、故意に危険なコードを含ませたソース(1のコード)をアップした

Consoleを見るに、1 の危険を含むコードは、下記のようなエラーが表示されていることが確認できる。

This document requires ‘TrustedHTML’ assignment.

Uncaught TypeError: Failed to set the ‘innerHTML’ property on ‘Element’: This document requires ‘TrustedHTML’ assignment.

反対に、2 の安全なコードでは、エラーは表示されない。

これらのことから、Trusted Types が予想通りに正しく機能していることが確認できた。

おわりに

Trusted Types を有効化しておくことで、開発者にとって安全なコードを書く助けとなることがわかった。

現在はまだ Draft のため Polyfill が必要だが、積極的に使用していきたい。

Share on

whasse
WRITTEN BY
whasse
Web Developer