@angular/router: サーバーサイドレンダリング(SSR)における、一致しない空パスアウトレットのルーティング設定によるサービス拒否(DoS)の脆弱性

Node.js (V8) でサーバーサイドレンダリング(SSR)が有効になっている `@angular/router` に、サービス拒否(DoS)の脆弱性が存在することが判明しました。

ルーティングの認識中に、`@angular/router` は、URL内の補助アウトレットセグメントが、特定のアウトレット名に対して設定されたルーティングと一致するかどうかを検証しません。 その結果、設定されていない空パスアウトレットグループ(例:`a:/()`, `x:/(...)`, または `///(...)`)が、ルーティングのマッチング時に拒否されず、ルーターはURLに含まれる任意の各アウトレットセグメントに対して、ルーティング設定を繰り返し再評価します。

リクエストURLに複数の任意の空パスアウトレットセグメントが含まれている場合、この不適切なマッチングにより、ルーターは重複した `ActivatedRouteSnapshot` ツリーを生成し、`canMatch` ガードやルートリゾルバーを繰り返し実行し、設定されていない各アウトレットに対して、ルーティングパラメータと `queryParams` の別々のコピーを割り当てます。

認証されていないリモート攻撃者は、この動作を悪用して、細工されたアウトレットセグメントを含むリクエストを送信することで、Node.jsのメモリ領域を枯渇させたり、CPUリソースを過負荷にしたりし、SSRワーカーの停止やサービスの中断を引き起こす可能性があります。

### 影響

攻撃が成功すると、認証されていないリモート攻撃者は、単一の細工されたリクエストまたは少数の同時リクエストによって、Node.jsのメモリ領域を枯渇させ、SSRワーカープロセスを致命的なJavaScriptのメモリ不足エラーで停止させることができます。 マッチしたルーティングで `canMatch` ガードやリゾルバーが宣言されている場合、単一の短いリクエストでも、繰り返しガードやリゾルバーの実行をトリガーし、バックエンドサービスへの負荷を増加させ、ワーカー接続を維持する可能性があります。

* **一時的なメモリ割り当て:** ルーティングの認識が完了すると、メモリは解放されます。そのため、永続的なメモリリークは発生しません。主な影響は、プロセスの終了です。
* **クライアントサイドのSPAには影響なし:** ブラウザで動作するクライアントサイドのAngularアプリケーションは影響を受けません。これは、ブラウザのメモリ使用量がセキュリティ境界を超えないためです。
* **プリレンダリング(SSG)には影響なし:** ビルド時のプリレンダリングは、攻撃者が提供したリクエストURLを処理しません。

### 攻撃の前提条件と脆弱な設定

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

1. **SSR(サーバーサイドレンダリング)が有効:** アプリケーションがNode.js/V8サーバー上でオンデマンドでレンダリングされる。
2. **脆弱なルーティング設定:** ルーティング設定の少なくとも1つのレベルに、以下のいずれかが含まれている。
* 空のパスを持つルート(例:`{path: "", component: ShopPage}`)
* 名前付きアウトレットに割り当てられたルート(例:`{path: "dashboard", children: [{path: ":id", outlet: "detail", component: DetailPanel}]}`)
3. **アップストリームからのリクエストの転送:** リクエストが、エッジプロキシによって拒否されずに、Node.jsプロセスに到達する。Node.jsは、デフォルトで最大16 KiBのリクエストヘッダーを受け入れます。ガード/リゾルバーの増幅には、短いURLのみが必要です。
4. **制限されたヒープサイズ:** ワーカーのヒープサイズが、リクエストのペイロードと並行性の相対的に小さく設定されている(例:128 MiB~1 GiBで構成されたコンテナまたはサーバーレスインスタンス)。

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

空のパスを持つルートとクエリパラメータを持つ設定に対する攻撃の例:

```http
GET /shop/(a:/()//b:/()//c:/()//...)?param1=value&param2=value... HTTP/1.1
Host: example.com
```

名前付きアウトレットまたは兄弟ルートを持つ設定に対する攻撃の例:

```http
GET /dashboard/(detail:1//x:/(detail:1//x:/(...)//y:/(...))//y:/(...)) HTTP/1.1
Host: example.com
```

### 修正方法

この問題は、[angular/angular#71055](https://github.com/angular/angular/pull/71055)で修正されています。ルーターは、子セグメントを展開したり、マッチングを完了する前に、各子アウトレットセグメントグループが、ターゲットアウトレットに設定されたルートと一致していることを要求し、設定されていない空のパスを持つアウトレットグループが、他のアウトレットで宣言されたルートと一致しないようにします。

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

v20より前のバージョンも影響を受けていますが、サポートは終了しており、修正は提供されません。

### 対策

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

1. **ミドルウェアまたはWAFで、括弧または空のパスを持つアウトレットのURLを拒否する(推奨):**
`@angular/router`では、URL内の補助アウトレットグループは常に括弧`(...)`を使用し、URL内の空のパスを持つアウトレットグループは、`:/( `、`(/ `、または`///(`のいずれかの形式を取ります。

* アプリケーションがURLで名前付き(補助)アウトレットを使用していない場合は、Angular SSRがリクエストを処理する前に、パスに`(`または`)`が含まれるリクエストを拒否してください。
* アプリケーションが名前付きアウトレットを使用している場合は、パスに空のパスを持つアウトレットのシーケンス(`:/( `、`(/ `、または`///(`)が含まれるリクエストを拒否してください。

URLの長さ制限とは異なり、この対策は、ヒープの枯渇と、短いURLのガード/リゾルバーの増幅を完全に防ぎます。

```typescript
// server.ts (Angular SSRハンドラーの前に配置)
app.use((req, res, next) => {
let pathname: string;
try {
pathname = decodeURI(req.url.split(/[?#]/, 1)[0]);
} catch {
res.status(400).send("Bad Request");
return;
}

// オプションA:アプリケーションがURLで名前付きアウトレットを使用していない場合は、すべての括弧を含むグループをブロックします。
if (/[()]/.test(pathname)) {
res.status(400).send("Bad Request");
return;
}
```

```javascript
// オプションB:アプリケーションが名前付きアウトレットを使用している場合、パスが空のアウトレットグループのみをブロックします。
if (/:\/\(|\(\//\(|\/\/\/\(/.test(pathname)) {
res.status(400).send("Bad Request");
return;
}

next();
});
```

2. **リバースプロキシまたはミドルウェアでURLの長さを制限する:
エッジプロキシ、ウェブアプリケーションファイアウォール(WAF)、またはリバースプロキシを設定し、過度に長いURLを持つリクエストを拒否します(例:2,048バイトを超える)。ペイロードのアウトレット部分はパスに含まれるため、クエリ文字列だけでなく、リクエスト全体のターゲットを制限します。これにより、ガード/リゾルバーの増幅効果を軽減しますが、完全に排除するわけではありません。短縮されたURLのみでこの問題が発生するためです。

3. **Node.jsのOld Spaceのサイズを増やす:
`--max-old-space-size`の値を大きくすると、メモリを使い切るために必要なリクエストサイズと同時実行性が向上しますが、この問題を完全に解決するわけではありません。

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