[解説] Angular SSRにおける深刻なDoS脆弱性 (GHSA-f67j-2jqw-jpq7 / CVE-2026-101895)
はじめに
日本のフロントエンドエンジニアの皆さん、こんにちは。今回は、Angularのサーバーサイドレンダリング (SSR) を利用しているアプリケーションに影響を及ぼす可能性のある、深刻なサービス拒否 (DoS) 脆弱性について詳しく解説します。この脆弱性 (GHSA-f67j-2jqw-jpq7 / CVE-2026-101895) は、`@angular/platform-server`が内部で使用するDOMエミュレーションパーサー`domino`に起因します。信頼できないユーザー入力を適切に扱わないと、Node.jsサーバーがフリーズし、アプリケーションが利用不能になる恐れがあります。
脆弱性の概要 (GHSA-f67j-2jqw-jpq7 / CVE-2026-101895)
この脆弱性は、`@angular/platform-server`がDOMエミュレーションのために利用している`domino`ライブラリのHTMLパーサーに存在します。特に、ユーザーから提供された信頼できない入力に、途中で途切れたDOCTYPE宣言(例: `<!DOCTYPE html `のように、宣言が途中で空白文字で終わりEOFに至るもの)が含まれていた場合、HTMLパーサーが同期的な無限ループに陥ります。
この無限ループにより、Node.jsのシングルスレッドプロセスはCPU使用率が100%に張り付き、イベントループが完全に枯渇します。結果として、サーバーは一切のHTTPリクエストに応答できなくなり、サービス拒否状態に陥ります。
技術的詳細: なぜ無限ループが発生するのか?
`domino`のHTMLパーサーは、`lib/HTMLParser.js`に実装されています。問題は、DOCTYPEを処理する際の特定のステートハンドラ、例えば`after_doctype_name_state`にあります。
このステートハンドラは、固定のルックアヘッド(先行読み込み)に依存しており、通常は文字インデックスポインタ (`nextchar`) を明示的に進めることで次の文字を処理します。しかし、EOF(End-Of-File、コードポイント-1)に遭遇した場合の処理に不備がありました。以下のコードスニペットが示すように、EOFのケースでは`nextchar`を進めることなく`emitDoctype()`と`emitEOF()`トークンを発行するだけでした。
```javascript case -1: // EOF forcequirks(); emitDoctype(); emitEOF(); break; ```
`nextchar`がEOFマーカー (`\uFFFF`) を指したまま更新されないため、パーサースキャナーのメインループ (`while (nextchar < numchars)`) は無限に`after_doctype_name_state`をEOF (`codepoint = EOF`) で再呼び出しし続けることになります。Node.jsのイベントループは完全にブロックされ、サーバープロセスはフリーズします。
影響と到達性
この脆弱性の**到達性**は広く、信頼できないユーザー入力がAngular SSRアプリケーションの以下の箇所で処理される場合、影響を受ける可能性があります。
<ul><li>テンプレートバインディングを通じて`[innerHTML]`にバインドされる場合</li><li>マークアップ内に補間される場合</li><li>サーバー側でDOM APIを通じてサニタイズされる場合</li></ul>
**影響**としては、未認証のリモート攻撃者が、不完全なDOCTYPE宣言を含むペイロード(例: `<!DOCTYPE html `)を送信するだけで、即座にサービス拒否 (DoS) を引き起こすことができます。Node.js SSRプロセスはCPU 100%でロックアップし、現在および将来のすべてのHTTPリクエストに応答できなくなります。これは、サイトの可用性に直接的な脅威となります。
脆弱性の再現 (Proof of Concept)
以下のAngularコンポーネントの例は、この脆弱性を容易に再現できます。信頼できない入力を`payload`として`[innerHTML]`にバインドしている典型的なケースです。
```typescript import { Component } from '@angular/core'; @Component({ selector: 'app-root', standalone: true, template: `<div [innerHTML]="payload"></div>`, }) export class AppComponent { // 攻撃者が制御する入力で、不完全なDOCTYPE宣言を含む payload = '<!DOCTYPE html '; } ```
この`AppComponent`がSSRされると、`domino`パーサーが`payload`の`<!DOCTYPE html `を処理する際に無限ループに陥り、Node.jsサーバーがクラッシュします。
対策と緩和策
この深刻なDoS脆弱性からアプリケーションを保護するためには、以下の対策を講じることが推奨されます。
1. <strong>信頼できないユーザー入力を`[innerHTML]`に直接バインドしない</strong>: これが最も重要かつ基本的な対策です。SSRテンプレートにおいて、未検証のユーザー入力を`[innerHTML]`に直接渡すことは避けてください。`[innerHTML]`は、HTMLをそのままレンダリングするため、XSS(クロスサイトスクリプティング)などの他の脆弱性も引き起こす可能性があります。
2. <strong>テキスト補間 (`{{ userInput }}`) または `[textContent]` を使用する</strong>: HTMLタグやエンティティをレンダリングする必要がない場合は、標準のテキスト補間 (`{{ userInput }}`) や`[textContent]`バインディングを使用してください。これらはAngularによって自動的にエスケープ処理されるため、HTMLパーサーレベルでの予期せぬ挙動を防ぎ、安全です。
3. <strong>サーバーサイドでの厳格な入力検証とサニタイズ</strong>: どうしてもHTMLとしてユーザー入力を表示する必要がある場合は、サーバーサイドで入力値を`[innerHTML]`に渡す前に、厳格に検証しサニタイズすることが必須です。具体的には、以下のいずれかの方法を検討してください。
<ul><li>入力の先頭が`/^<!DOCTYPE/i`の正規表現にマッチする場合、その入力を拒否するか、DOCTYPE宣言部分を完全に除去する。</li><li>信頼できるHTMLサニタイザーライブラリ(例: `dompurify`など)を適用し、不適切なHTML構造やDOCTYPE宣言を事前に除去する。ただし、サニタイザーを通す前に`domino`のパーシングで無限ループが始まる可能性も考慮し、DOCTYPE宣言自体のチェックが重要です。</li></ul>
これらの対策を組み合わせることで、アプリケーションの堅牢性を高め、DoS攻撃のリスクを大幅に軽減できます。
まとめ
Angular SSRを利用している開発者の皆さんにとって、今回のDoS脆弱性は特に注意が必要です。信頼できないユーザー入力の取り扱いは、常にセキュリティ上のリスクを伴いますが、`[innerHTML]`のような危険なAPIを使用する際には一層の警戒が求められます。ご紹介した対策を参考に、皆さんのアプリケーションのセキュリティを確保し、ユーザーが安心して利用できる環境を維持しましょう。
セキュリティは継続的な取り組みです。最新の脆弱性情報に常にアンテナを張り、適切なアップデートや対策を迅速に適用していくことが、私たちフロントエンドエンジニアの重要な責務の一つです。