Trusted Types
Trusted Types (TT) とは
DOM Based XSS を緩和するための機能。次のような危険な Sink にデータを渡す前に処理する(Trusted Type 型に変換する)ことができる。
<script src>
及び<script>
のテキストコンテンツの設定- 文字列から HTML を生成するもの
innerHTML
outerHTML
inserAdjacementHTML
<iframe> srcodc
document.write
document.writeln
DOMParser.parseFromString
- プラグインの実行
<embed src>
<object data>
<object codebase>
- JavaScript コードの実行
eval
setTimeout
setInterval
new Function()
// ❌ TypeError element.innerHTML = location.href; // ✅ TrustedHTML Object のみ可能 element.innerHTML = trustedHTML;
Trusted Types の使い方
Example
<?php header("Content-Security-Policy: trusted-types example dompurify; require-trusted-types-for 'script'"); ?> <html> <head> <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.2.6/purify.min.js" integrity="sha512-rXAHWSMciPq2KsOxTvUeYNBb45apbcEXUVSIexVPOBnKfD/xo99uUe5M2OOsC49hGdUrkRLYsATkQQHMzUo/ew==" crossorigin="anonymous"></script> </head> <body> <script> const policy = trustedTypes.createPolicy('example', { createHTML: (untsutedValue) => { return DOMPurify.sanitize(untsutedValue, {RETURN_TRUSTED_TYPE: true}) } }); // http://localhost/#'%22%3E%3Csvg/onload=alert(1)%3E const rawHTML = decodeURIComponent(location.hash.substring(1)); // document.body.innerHTML = rawHTML; document.body.innerHTML = policy.createHTML(rawHTML); </script> </body> </html>
当然、 meta
タグでも可能。
<meta http-equiv=content-security-policy content="require-trusted-types-for 'script'; trusted-types example dompurify">
report-only で適用する
Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; report-uri //my-csp-endpoint.example
Enforce
Content-Security-Policy: require-trusted-types-for 'script'; report-uri //my-csp-endpoint.example
違反レポートの収集
window.addEventListener('securitypolicyviolation', console.error.bind(console));
ポリシーの作り方
自前でポリシーを用意する。
// createHTML() 関数を介して、TrustedHTML オブジェクトを生成できる myEscapePolocy を定義 // trustedTypes.createPolicy() に渡される JavaScript 関数は文字列を返すが、 // createPolocy() は戻り値を TrustedHTML などの正しい型で wrap する if (window.trustedTypes && trustedTypes.createPolicy) { const escapeHTMLPolicy = trustedTypes.createPolicy('myEscapePolicy', { createHTML: string => string.replace(/\</g, '<') }); }
const escaped = escapeHTMLPolicy.createHTML('<img src=x onerror=alert(1)>'); console.log(escaped instanceof TrustedHTML); // true el.innerHTML = escaped; // '<img src=x onerror=alert(1)>'
DOMPurify は Trusted Types をサポートしているので、Sanitize 結果として TrustedHTML
オブジェクトを返すことができる。
if (window.trustedTypes && trustedTypes.createPolicy) { // Feature testing trustedTypes.createPolicy('default', { createHTML: (string, sink) => DOMPurify.sanitize(string, {RETURN_TRUSTED_TYPE: true}) }); }
この辺は Sanitize API が普及すると、それを使うようになるだろう。
default
ポリシー
default
というポリシー名を利用すると、Sink にデータが渡されると暗黙的に呼び出される。第一引数は文字列値、第二引数はシンク名となる。
TrustedType
trustedHTML
... HTML としてレンダリングしてよい信頼された文字列。TrustedTypePolicy
のcreateHTML
によって生成される。trustedScript
... Script として実行してよい信頼された文字列。TrustedTypePolicy
のcreateScript
によって生成される。trustedScriptURL
... 外部のスクリプトリソースとして信頼された文字列。TrustedTypePolicy
のcreateScriptURL
によって生成される。
ブラウザの対応状況
2020/05/08 現在
Chrome 83 で対応。他のブラウザでは polyfill で対応できる。
ライブラリでの対応
当然使っているライブラリで危険な Sink を使っている場合は対応が必要になる。
e.g. https://github.com/bvaughn/react-virtualized/pull/1614
Security
色々バイパスする方法はあるっぽい。
Trusted Types bypass challenge solutions | shhnjk.github.io
Blob URL
const blob = new Blob([untrustedString], {type: 'text/html'}); const url = URL.createObjectURL(blob); location = url;
Blob URL は TT で制限できない。その理由は次の通り。
type
引数は安全なもの (text/plain
など) にすることができるURL.createObjectURL
は File オブジェクトと MediaSource オブジェクトも受け入れる
Non-DOM API Based script
DOM API ではないスクリプトの読み込みは TT で制限できない。
import(`/${untrustedString}.js`)
Cross Document
TT は HTMLAnchorElement.href
や HTMLIFrameElement.src
などの sink に JavaScript URL を代入することをブロックしない。
CSP と同じように、TT が適用されたドキュメントで JavaScript URL への navigation をブロックするようになっている。
しかしながら、CSP でも既知の問題である Cross Document 間での相互作用の問題があり、一方のドキュメントに TT が適用されていない場合、もう一方の TT を Bypass できてしまう。これは https://wicg.github.io/origin-policy/ で対応しようとしている。
具体例は次の通り。robots.txt
に TT が適用されていない場合、JavaScript URL は robots.txt の iframe のドキュメントで実行されるので、XSS が生じる。
<!DOCTYPE html> <html lang="en"> <head> <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.2.6/purify.min.js" integrity="sha512-rXAHWSMciPq2KsOxTvUeYNBb45apbcEXUVSIexVPOBnKfD/xo99uUe5M2OOsC49hGdUrkRLYsATkQQHMzUo/ew==" crossorigin="anonymous"></script> <meta http-equiv=content-security-policy content="require-trusted-types-for 'script'; trusted-types default dompurify"> </head> <body> <iframe src="robots.txt" name="new"></iframe> <script> trustedTypes.createPolicy('default', { createHTML: (untsutedValue) => { return DOMPurify.sanitize(untsutedValue, {RETURN_TRUSTED_TYPE: true}) }, createScriptURL: (url) => { return "" } }); const url = "javascript:alert(origin)"; const a = document.createElement('a'); a.href = url; a.target = 'new'; a.textContent = 'go'; document.body.appendChild(a); </script> </body> </html>
HTMLAnchorElement properties (Polyfill only)
2020/05/08 現在だと Polyfill の場合は次のような形でバイパスが可能。
const policy = trustedTypes.createPolicy('default', { createScriptURL: (untsutedValue) => { return ''; } }); const rawHTML = decodeURIComponent(location.hash.substring(1)); const a = document.createElement('a'); a.href = policy.createScriptURL('http://example.com'); a.pathname = '\nalert(1)'; a.protocol = 'javascript:'; document.body.appendChild(a); a.click();
その他
よく tt
と略されるので issue とか追いかけるときは頭に入れておくといい。