ウェブアプリケーションの高度化に伴い、セキュリティに対する関心も年々高まりつつあります。特に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, '<').replace(/>/g, '>');
}
});
const elem = document.createElement('div');
elem.innerHTML = myPolicy.createHTML('<div>Hello!</div>');
document.body.appendChild(elem);
上の例では my-policy
という名前のポリシを作成しています。ポリシ名は任意ですが、わかりやすい名前をつけておきましょう。
次に Content-Security-Policy
の truted-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, '<').replace(/>/g, '>');
}
});
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は使用可能です。完全なエラーにしたくはない場合はこちらを使いましょう。