/tmp/

雑なメモを置く場所。書いた内容の責任は取らないし、正確性、永続性なども保証しない。

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, '&lt;')
  });
}
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)>'

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 としてレンダリングしてよい信頼された文字列。 TrustedTypePolicycreateHTML によって生成される。
  • trustedScript ... Script として実行してよい信頼された文字列。 TrustedTypePolicycreateScript によって生成される。
  • trustedScriptURL ... 外部のスクリプトリソースとして信頼された文字列。 TrustedTypePolicycreateScriptURL によって生成される。

ブラウザの対応状況

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.hrefHTMLIFrameElement.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 の場合は次のような形でバイパスが可能。

github.com

    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 とか追いかけるときは頭に入れておくといい。

References