Marginalia

Angular: Trusted Typesサポートの概要

DOM の新しいセキュリティ機構として Trusted Types という仕様が提案されている。 現在開発中の Angular v11 は Trusted Types の仕様に準拠し、Trusted Types をサポートしたブラウザではその機能が利用できるようになる予定だ。 この記事では Angular と Trusted Types がどのように関わるのかを解説する。

Trusted Types とは

Trusted Types そのものが初見であれば、Jxck さんのブログ記事を先に読むことをおすすめする。

安全な文字列であると型で検証する Trusted Types について | blog.jxck.io

簡潔に言えば、文字列ベースでの DOM 操作が可能な API について、その文字列が信頼できるものであることをマークして、ブラウザに対してその DOM 操作が安全である(と開発者は信じている)ことを伝えるものだ。 具体的な API で言えば、 element.innerHTMLscript.src などが挙げられる。

CSPと併用することで、信頼できることがマークされていない文字列はブラウザがセキュリティポリシー違反としてエラーを報告できるようになる。 検証目的であればindex.html<head>タグ内に次の HTML を追加すれば簡単に Trusted Types の動作を確認できる。

<meta
  http-equiv="Content-Security-Policy"
  content="require-trusted-types-for 'script';"
/>
<script>
  window.addEventListener(
    'securitypolicyviolation',
    console.error.bind(console)
  );
</script>

Angular と Trusted Types

実は以前にも Angular と Trusted Types について記事を書いている。

DOM の XSS を防ぐ Trusted Types と Angular のセキュリティ機構

この記事では Angular がビルトインで持つ独自のセキュリティ機構と、Trusted Types がどのようにかかわる可能性があるかを少し述べた。 まず Angular の DomSanitizer についての基本的な理解が必要となるため、理解が不安であればまずはこちらを読んでほしい。

DomSanitizer と Trusted Types

DomSanitizer は、任意の文字列が信頼できる HTML、スクリプト、CSS などであることを、開発者が Angular に伝えるための API だ。 たとえば innerHTML への文字列の挿入で <iframe> タグを残してほしければ、次のように文字列が安全な HTML であるとマークする。

import { Component } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';

const unsafeHTML = `<iframe src="https://example.com" width="100%" height="500"></iframe>`;

@Component({
  selector: 'app-root',
  template: ` <div [innerHTML]="snippet"></div> `,
})
export class AppComponent {
  constructor(private sanitizer: DomSanitizer) {}

  snippet = this.sanitizer.bypassSecurityTrustHtml(unsafeHTML);
}

DomSanitizer.bypassSecurityTrustHtml は引数に与えた文字列を SafeHtml 型としてラップし、サニタイズ処理をバイパスする。 もしinnerHTML にバインディングされた htmlSnippet が単なる string であれば、 <script>タグや<iframe>タグのような XSS の危険性のあるタグはサニタイザーによって除去される。 一方、すでに安全であるとマークされた SafeHtml型であれば、Angular はその値を信じてサニタイズせずにそのまま挿入する。

当然だが、バイパスする場合 HTML の安全性、XSS の回避は完全に開発者の責任となることは留意しなくてはならない。 特に user-generated な文字列を挿入するケースでは、独自のサニタイズ処理を行うことは必須だろう。

<script>タグを含むHTMLをバイパスして innerHTML に挿入されても何も動作しないように見えるが、これはサニタイズされたのではなく <script> タグの仕様である。 <script> タグのノード自体は作成されているが、実行はされない。動的な <script> タグの挿入は document.createElement などのAPIを使おう。

innerHTML へのテンプレートバインディングから実際に DOM に挿入されるまでの流れを簡単に模式化すると次のようになる。 処理の流れはバインディングされた HTML が string か SafeHtml かでサニタイズの有無が変わるが、 もし CSP で Trusted Types が要求されていれば、どちらにしてもブラウザからすれば信頼できない値としてエラーとなる。 つまり、これまで Angular アプリケーションでは Trusted Types を満足に利用することは難しかった。

innerHTML binding and sanitization
innerHTML binding and sanitization

“angular” Trusted Types ポリシー

Angular に新しく実装される Trusted Types のサポートでは、上記のようなケースが CSP エラーとならないようにする。 Angular 側の課題はセキュリティ強度やサニタイズの中身ではなく、 Angular 側ですでに信頼済みであることをブラウザに伝えられていない ということであるため、 基本的なサニタイズの処理にはほとんど手は加わらない。

最終的な DOM 操作の前に、Angular は対象の文字列を Trusted HTML に変換するようになるが、このとき使用されるポリシーは、サニタイズがバイパスされているかどうかで変わる。 バイパスされず Angular が組み込みのサニタイザーを通した安全な文字列は angular ポリシーで Trusted HTML に変換される。 一方、Angular によるサニタイズをバイパスした場合は angular#unsafe-bypass ポリシーで変換される。

innerHTML binding and sanitization with trusted types
innerHTML binding and sanitization with trusted types

現在の実装では名前以外にはポリシーの中身に違いはない。 これは Angular 標準のサニタイザーを通した操作とそうでない操作が依存するポリシーを分離し、angular ポリシーは Angular によって真に信頼されていることをソースコード上で明確にしているだけだ。 将来的には開発者が自分で Trusted Types ポリシーを設定できるカスタムサニタイザーを可能にする予定があり、そのための布石でもあるようだ。

つまり、今のところは開発者が Angular の Trusted Types サポートに関して何かを行う必要はないし、逆に何か介入することも難しい。 いままで存在した Angular 内部のセキュリティ機構がブラウザとも連携するようになったというだけの話である。

まとめ

v11.0 の RC バージョンが開始してリリースが近づいてきたが、Trusted Types について理解しておきたいのは以下の点だ。

  • Angular のテンプレートを介した DOM 操作が Trusted Types の仕様に準拠するようになる
  • したがって、Angular のテンプレートバインディングを介した DOM 操作で Trusted Types 違反が起きなくなる
  • HTML サニタイズの振る舞いや DomSanitizer の使い方はこれまでと変わらない
  • ユーザー独自の Trusted Types ポリシーと連携する手段はまだ無い
  • これまで同様、Angular が保護できるのはテンプレートを介したセキュリティだけであり、スクリプトによる DOM 操作はスコープ外である

Angular の Trusted Types サポートの動きを追跡したければ、GitHub のイシューを購読するとよい。

[tracking] Support Trusted Types in Angular by bjarkler · Pull Request #39222 · angular/angular