Subterranean Flower

Trusted Typesを利用してJavaScriptからのDOM操作をセキュアに行う

Author
古都こと
ふよんとえんよぷよぐやま!!!

ウェブアプリケーションの高度化に伴い、セキュリティに対する関心も年々高まりつつあります。特にXSS(クロスサイトスクリプティング)と呼ばれる脆弱性は簡単ながらも大きな被害をもたらします。アプリケーションの開発者は当然セキュリティを意識した開発を行うべきですが、人間の注意は万能ではなく、時に不注意から脆弱なアプリケーションを作成してしまいます。

こういった状況を改善するために、Trusted Typesという提案がなされています。Trusted Typesはよりセキュアなウェブアプリケーションを作る手段を提供し、安全性を高める補助をしてくれます。

Trusted Types

HTMLやJavaScriptは非常に柔軟な仕組みを有しており、要素を動的に組み立てることが可能です。例えば以下の例を見てみましょう:

const { username, email } = await api.getUser();
const userInfo = document.createElement('div');
userInfo.innerHTML = `${username}, ${email}`;

このコードはAPIからユーザ情報を取得し、div要素に取得した情報を追加しようとしています。

ここで innerHTML は代入された文字列をHTMLとして解釈します。すると大きな問題が起こります。もし何らかの原因でユーザ名やメールアドレスに悪意ある値、例えばスクリプトなど、が埋め込まれていた場合、ブラウザは何の疑いもなくそのスクリプトを実行します。これがXSSです。

XSSが発生する原因の多くは、主にDOMが任意の文字列を許容するところにあります。任意の値を受け取るということは、悪意あるコードが一切の検査を通らずに実行されてしまうことを意味します。

Trusted Types はDOMのプロパティなどが 任意を文字列を受け取ることを禁止 し、特定の関数を通過した 検査済み文字列のみを許容 するようにする機能です。

Trusted Typesを有効活用することで、より安全なアプリケーションが開発できます。

ブラウザ対応状況

Trusted TypesはChrome 83より利用可能になります。他のブラウザにおける対応は未定です。

Trusted Typesの有効化

Trusted Typesを利用するにはhttps環境またはlocalhostでの実行である必要があります。

Trusted Typesはデフォルトで無効となっており、利用のためには有効化する必要があります。有効化のためにはHTTPの Content-Securiy-Policy ヘッダで require-trusted-types-for 'script'trusted-typesを指定します。

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

HTTPヘッダの他にも <meta> 要素でも指定できるので、簡単なテストの際にはこちらでもよいでしょう。

<meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script'; trusted-types">

これでTrusted Typesが有効化されます。試しに以下のようなコードを実行してみます:

const elem = document.createElement('div');
elem.innerHTML = 'Hello!';
document.body.appendChild(elem);

すると次のエラーが出ます:

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

Trusted Typesが有効化されたため、もう innerHTML は単なる文字列を受け付けません。文字列を代入しようとするとエラーとなります。エラー文の通り、文字列ではなく TrustedHTML 型の値を代入しなければいけません。

Trusted Typesの対象となるのは以下のものです:

  • document

    • write メソッド
    • writeln メソッド
  • <script>

    • innerText プロパティ
    • textContent プロパティ
    • src プロパティ
    • text プロパティ
  • <iframe>

    • srcdoc プロパティ
  • <embed>

    • src プロパティ
  • <object>

    • data プロパティ
    • codeBase プロパティ
  • SVG

    • href プロパティ
  • 全てのHTML要素

    • outerHTML プロパティ
    • insertAdjacentHTML メソッド
    • innerHTML プロパティ
  • Range

    • createContextualFragment メソッド
  • DOMParser

    • parseFromString メソッド
  • Timer系関数

    • setTimeout 関数
    • setInterval 関数
  • Web Workers

    • new Worker() コンストラクタ
    • new SharedWorker() コンストラクタ
    • importScripts 関数
  • Service Worker

    • register メソッド

ポリシの作成

Trusted Typesは有効化しましたが、当然ながらこのままでは何も操作できません。何かしらの方法で TrustedHTML 型の値を生成する必要があります。2通りの方法が考えられます:

  • DOMPurifyのようなTrusted Types対応のライブラリを使用する
  • Trustedな値を生成するポリシを自作する

今回はポリシの自作をしてみましょう。ポリシの作成には、グローバルに trustedTypes というオブジェクトができているので、それの createPolicy メソッドを利用します。

const myPolicy = trustedTypes.createPolicy('my-policy', {
  createHTML: (unsafeValue) => {
    return unsafeValue.replace(/</g, '&lt;').replace(/>/g, '&gt;');
  }
});

const elem = document.createElement('div');
elem.innerHTML = myPolicy.createHTML('<div>Hello!</div>');
document.body.appendChild(elem);

上の例では my-policy という名前のポリシを作成しています。ポリシ名は任意ですが、わかりやすい名前をつけておきましょう。

次に Content-Security-Policytruted-types ディレクティブでポリシを許可します。例えばポリシ名が my-policy であれば、trusted-types my-policy と記述します。

<meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script'; trusted-types my-policy">

ポリシが複数存在する場合は trusted-types my-policy my-policy2 my-policy3 のようにします。

ポリシが持つことができるメソッドは以下の3つです。

  • createHTML メソッド: TrustedHTML 型を返す
  • createScript メソッド: TrustedScript 型を返す
  • createScriptURL メソッド: TrustedScriptURL 型を返す

これらのメソッドを通した値はTrusted(信頼できる)な型になります。それぞれのメソッドを通すことで、信頼できるHTML文字列として扱える TrustedHTML 型や、信頼できるURLである TrustedScriptURL 型を得ることができます。

今回は innerHTML に代入する TrustedHTML が欲しいので、 createHTML のみを利用しています。必要に応じて他のメソッドも実装してください。例えば <script> 要素の src プロパティに代入できる TrsutedScriptURL が欲しい場合は createScriptURL メソッドを実装します。

作成したポリシのメソッド(先ほどの例では myPolicy.createHTML メソッド)を呼び出すことで実際にTrustedな値を得られます。

デフォルトポリシ

ポリシ名として default を指定すると特殊な挙動をします。これをデフォルトポリシと言います。

デフォルトポリシはTrustedではない普通の文字列が代入された時に、自動的に適用する処理を記述します。デフォルトポリシが存在しない場合はエラーになりましたが、デフォルトポリシが定義されていると自動変換してくれるのでエラーにはなりません。

ポリシの作り方やポリシの許可の仕方は同じです。

trustedTypes.createPolicy('default', {
  createHTML: (unsafeValue) => {
    return unsafeValue.replace(/</g, '&lt;').replace(/>/g, '&gt;');
  }
});

const elem = document.createElement('div');
elem.innerHTML = '<div>Hello!</div>';
document.body.appendChild(elem);

もちろん default という名前のポリシに対しての明示的な許可が必要です。

<meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script'; trusted-types default">

全てのポリシの許可

Content-Security-Policy から trusted-types ディレクティブを取り除くことで全てのポリシを許可することができます。

<meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script'">

ただしXSSで勝手にポリシを作られる可能性も考慮すると、この選択は賢いとは言えません。なので trusted-types ディレクティブはできるだけ指定するようにしましょう。

Trusted Typesの安全性

注意点として、Trusted Typesそれ自体はセキュアではないというところです。あくまで開発者に安全のための補助機構を提供するだけであり、自動的にサニタイズしてはくれませんし、全くの安全を保証してくれるわけではありません。

例えば以下のような危険なポリシを作成できます:

const myPolicy = trustedTypes.createPolicy('my-policy', {
  createHTML: (unsafeValue) => unsafeValue
});

このポリシは文字列をそのまま TrustedHTML として返しているだけであり、一切の無害化を行っておりません。よって悪意ある文字列を含んだ可能性のある値がそのままTrustedな値として扱われます。

Trusted Typesを使用する時は、値の扱いに気をつけましょう。

Content-Security-Policy-Report-Only での使用

Content-Security-Policy と同様に、当然ながら Content-Security-Policy-Report-Only でもTrusted Typesは使用可能です。完全なエラーにしたくはない場合はこちらを使いましょう。