@angular/router: サーバーサイドレンダリング(SSR)におけるRouterLinkのクエリパラメータ保持によるサービス拒否(DoS)の脆弱性

Node.js (V8) でサーバーサイドレンダリング(SSR)が有効になっており、`RouterLink`ディレクティブが `queryParamsHandling: 'merge'` または `'preserve'` でレンダリングされている場合に、`@angular/router` にサービス拒否(DoS)の脆弱性が存在することが判明しました。これらの設定は、リンクごとに、またはグローバルに `defaultQueryParamsHandling` を介して設定されます。

v21.2.0 以降 ([angular/angular#66181](https://github.com/angular/angular/pull/66181))、`RouterLink` の内部構造がシグナルを使用してリファクタリングされ、計算されたシグナル (`_urlTree`) が導入されました。このシグナルは、ディレクティブ内で返された `UrlTree` インスタンスをキャッシュします。各レンダリングされたリンクは、リクエストのライフサイクル全体を通してアクティブな `RouterLink` インスタンスを保持するため、計算されたシグナルは、SSR の応答が完了するまで、`UrlTree` と関連するクエリパラメータ辞書をメモリ上に固定します。

クエリパラメータの処理が `'merge'` または `'preserve'` に設定されている場合、各リンクは受信したリクエストのクエリパラメータを自身の `UrlTree` にコピーします。SSR 中、メモリ消費量は乗算的に増加します。

$$\text{メモリ保持量} \approx (\text{影響を受けるリンクの数}) \times (\text{解析されたクエリパラメータマップのサイズ})$$

解析された JavaScript オブジェクトとプロパティ辞書は、生のクエリ文字列よりもはるかに多くのメモリを必要とします。特に、プロパティ名が V8 のプロパティストレージのヒューリスティックによって、密な配列の割り当てをトリガーする場合、攻撃者は、リクエストごとにメモリ消費量を桁違いに増大させるために、非常に大きなクエリ文字列を送信することができます。

### 影響

この脆弱性が悪用されると、認証されていないリモート攻撃者が、比較的少ない同時リクエスト数で Node.js のヒープメモリを枯渇させ、回復不能な JavaScript ヒープのメモリ不足によるエラーで SSR 処理を停止させることができます。

* **数値のパラメータ名が不要:** 数値のクエリパラメータ名は、V8のプロパティストレージの割り当て量を大きく増加させます(テストでは、解析された辞書あたり約558KB、数値でない場合は約197KB)。これにより、クラッシュを引き起こすために必要な同時実行数の閾値が低下します。ただし、数値でないクエリパラメータも、ヒープの消費量を大幅に増加させ、比例して高い同時実行数でワーカーメモリを枯渇させる可能性があります。
* **クライアントサイドのSPAは影響を受けない:** ローカルブラウザのメモリ消費量がセキュリティ境界を超えないため、純粋なクライアントサイドのAngularアプリケーション(SSRを使用しないシングルページアプリケーション)は影響を受けません。

### 攻撃の前提条件と脆弱な構成

アプリケーションが影響を受けるのは、以下のすべての条件を満たす場合のみです。

1. **SSRが有効:** アプリケーションが、Node.js/V8によって提供されるサーバーサイドレンダリング環境で実行されている。
2. **マージまたは保持されたクエリパラメータ:** SSR中にレンダリングされる`RouterLink`要素が、`queryParamsHandling: 'merge'`または`'preserve'`を使用している。これは、リンクに明示的にバインドされるか、`withRouterConfig({ defaultQueryParamsHandling: 'merge' })`によってアプリケーション全体で構成されている。
3. **アップストリームプロキシが長いリクエスト行を転送:** リバースプロキシまたはロードバランサー(例:Nginx、AWS ALB、Cloudflare)が、エッジで拒否する代わりに、長いクエリ文字列をNode.jsアプリケーションに転送する。 (例:Nginxのデフォルトの8KBの`large_client_header_buffers`制限により、16KBのリクエスト行は、明示的に増加しない限り拒否されます)。
4. **オーバーラップしたレンダリング:** 保持されているメモリは、各リクエストが完了すると解放されます。したがって、複数の同時実行リクエストが、同じNode.jsワーカーインスタンス内で同時に処理される必要があります(例:非同期データ取得、ルートリゾルバー、またはSSR中の外部API呼び出しを待機しているページ)。

### 攻撃ペイロードの例

攻撃者が、複数のマージされたリンクをレンダリングするSSRページに対して、密度の高いクエリパラメータを含む同時HTTPリクエストを送信します。

```http
GET /products?0&17&34&51&68&85&...&48042 HTTP/1.1
Host: example.com
```

### 修正

この問題は、`RouterLink` からの永続的な `UrlTree` の保持を削除することで解決されます ([angular/angular#70953](https://github.com/angular/angular/pull/70953))。 `RouterLink` は、`reactiveHref` で直接ルートの依存関係を追跡し、シリアル化された href 文字列を計算した直後に、中間 `UrlTree` インスタンスをすぐに破棄します。 `urlTree` のゲッターとクリックハンドラは、必要に応じて `UrlTree` インスタンスを生成します。

その結果、中間的なクエリパラメータマップは、リクエストの存続期間中にヒープに保持されるのではなく、若い世代の GC スキャン中に短期間のみ割り当てられ、Angular 21.1 以前のメモリプロファイルが回復されます。

修正されたバージョン:
* `22.2.1`
* `21.2.25`

21.2.0 より前のバージョン(Angular 20.x LTS および 21.1.x を含む)では、`RouterLink` で `UrlTree` インスタンスを保持しておらず、この問題の影響を受けません。

### 対策

修正されたバージョンへのアップグレードがすぐにできない場合は、以下のいずれかまたは複数の対策を適用してください。

1. **サーバーレンダリングリンクでの 'merge' および 'preserve' の使用を避ける:**
グローバル設定から `defaultQueryParamsHandling: 'merge'` (または `'preserve'`) を削除します。 各リンクが必要とする特定のクエリパラメータのみを明示的にバインドします (例: `[queryParams]="{ filter: filter() }"`)。 修正されたバージョンであっても、この方法は、リクエストから派生したパラメータが生成された DOM マークアップに不必要に伝播するのを防ぎます。

2. **リバースプロキシまたはミドルウェアでクエリ文字列の長さを制限する:**
エッジプロキシ、Web アプリケーションファイアウォール (WAF)、またはリバースプロキシを設定して、過度に長いクエリ文字列 (例: 2,048 バイトを超える) を含むリクエストを拒否します。

代替案として、Angularのサーバーサイドレンダリング(SSR)がリクエストを処理する前に、Node.jsで過剰なクエリ文字列を拒否することができます。
```ts
// server.ts (Angular SSRハンドラの前)
app.use((req, res, next) => {
const queryIndex = req.url.indexOf('?');
if (queryIndex !== -1 && req.url.length - queryIndex > 2048) {
res.status(414).send('URI Too Long');
return;
}
next();
});
```

3. **Node.jsのOld Spaceのサイズを増やす:**
`--max-old-space-size`の値を大きくする(例:2048 MBまたは4096 MB)と、メモリの余裕が増え、メモリを使い切るまでに必要な同時実行数の閾値も高くなります。ただし、これにより、継続的なトラフィック下での脆弱性が完全に解消されるわけではありません。

影響を受けるパッケージ:
- npm (JavaScript) @angular/router: 影響を受けるバージョン >= 22.0.0、< 22.2.1、22.2.1で修正済み
- npm (JavaScript) @angular/router: 影響を受けるバージョン >= 21.2.0、< 21.2.25、21.2.25で修正済み