@nestjs/platform-fastify: 絶対パス形式のリクエストターゲットによる、パス指定ミドルウェアの回避

| フィールド | 値 |
|---|---|
| エコシステム | npm |
| パッケージ | `@nestjs/platform-fastify` |
| 影響を受けるバージョン | `>= 12.0.0, < 12.0.2` および `< 11.2.4` |
| 修正されたバージョン | `12.0.2` および `11.2.4` (バージョンを `12.0.3` / `11.2.5` にアップグレード) |

### 概要

Fastifyアダプタにおいて、**絶対パス形式のリクエストターゲット**(例:`GET http://host/path HTTP/1.1`、`/path HTTP/1.1` ではなく)を使用するHTTPリクエストが、そのルートにバインドされているパス指定のNestミドルウェアを実行せずに、ルートハンドラに到達する問題があります。ミドルウェアで認証または認可を強制するアプリケーションでは、これらのチェックを省略して保護されたハンドラが実行されます。

### 影響

以下の条件を満たすアプリケーションに影響があります。

- `@nestjs/platform-fastify`を使用していること
- `MiddlewareConsumer.forRoutes(...)`または`.exclude(...)`を使用して、ミドルウェアを特定のパスにバインドしていること
- クライアントが生のHTTPリクエストヘッダーを制御できる状態であること

Node.jsのHTTPサーバーは、絶対パス形式のターゲットを受け入れるため、特別なサーバー設定は不要です。リバースプロキシがアプリケーションの前に配置され、リクエストターゲットを相対パス形式に書き換える場合、この問題の影響は軽減されます。

回避されたミドルウェアが認証または認可を実行している場合、認証または認可の回避につながります。ミドルウェアがロギング、レート制限、またはボディの処理を実行している場合、それらは黙ってスキップされます。

### 詳細

Fastifyのルーター (`find-my-way`) は、マッチングの前に絶対パス形式のターゲットをパスに変換するため、ルートハンドラは通常通り実行されます。しかし、ミドルウェア側では、**生の**リクエストターゲットに対してマッチングが行われる2つの箇所が存在します。

1. `@fastify/middie` エンジンのバンドル版は、`packages/platform-fastify/adapters/middie/fastify-middie.ts` にあります。
NestJS は、以前のパスデコードの修正を適用するためにこのフォークを使用しましたが、`@fastify/middie@9.3.4` でリリースされたアップストリームの絶対パス形式の修正は追跡されていませんでした。
2. `FastifyAdapter` 自体の `createMiddlewareFactory()` 内の再チェック機能があり、ここでミドルウェアのパス正規表現を `req.originalUrl` と比較しています。

どちらもターゲットを正規化およびパーセントデコードしましたが、どちらも絶対パス形式をパスに変換しなかったため、ルーターとミドルウェア層が要求されているパスについて異なる認識をしていました。

さらに、関連する別の問題も存在しました。アダプターは常に `routerOptions` を渡すため(バージョン制約を適用するため)、Fastify は非推奨となった**トップレベル**のルーターオプション (`ignoreTrailingSlash`, `ignoreDuplicateSlashes`, `caseSensitive`, `useSemicolonDelimiter`) を `initialConfig.routerOptions` に反映していませんでした。この `initialConfig.routerOptions` はミドルウェアエンジンが参照します。トップレベルでこれらのオプションを渡すアプリケーションでは、ルーターとは異なるパスの正規化が行われるため、不整合が拡大しました。

### 実証コード

```ts
@Controller('users')
export class UsersController {
@Get()
findAll() {
return 'protected data';
}
}

@Module({ controllers: [UsersController] })
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply((req, res) => res.end('blocked by auth middleware'))
.forRoutes({ path: 'users', method: RequestMethod.GET });
}
}
```

`supertest` と `light-my-request` は常に origin-form のターゲットを生成するため、リクエストをソケットに書き込む必要があります。

```js
const { connect } = require('node:net');

const socket = connect(3000, '127.0.0.1', () => {
socket.write(
'GET http://127.0.0.1:3000/users HTTP/1.1\r\n' +
'Host: 127.0.0.1:3000\r\n' +
'Connection: close\r\n\r\n',
);
});
socket.pipe(process.stdout);
```

影響を受けたバージョンで確認された現象:`protected data` — ミドルウェアが実行されませんでした。
修正されたバージョンで期待される現象、および確認された現象:`auth middleware`によってブロックされます。

### 修正内容

**12.0.2**および**11.2.4**で修正されました。**12.0.3**または**11.2.5**へのアップデートを推奨します。

- バンドルされていた`@fastify/middie`の派生版が削除され、パッケージは現在`@fastify/middie@9.3.4`に依存しています。これにより、絶対パスのターゲットが一致する前に解決されます。
- アダプタは、自身のルートチェックで絶対パスのターゲットを解決し、`find-my-way`の動作を模倣しています。
- 廃止されたトップレベルのFastifyのルーティングオプションが`routerOptions`に統合され、ミドルウェアエンジンとルーティングがパスを同一化するように変更されました。

### 対策

アップグレードできない場合は、ミドルウェアが実行される前に、非オリジンフォームのリクエストターゲットを拒否してください。Fastifyインスタンスに以下のフックを、アプリケーションの初期化**前**に登録することで、ミドルウェアエンジンの独自の`onRequest`フックよりも先に実行され、上記の例のように、拒否が有効になっていることを確認できます。

```ts
const adapter = new FastifyAdapter();
adapter.getInstance().addHook('onRequest', (request, reply, done) => {
const target = request.raw.url ?? '';
// "*"は"OPTIONS * HTTP/1.1"の有効なリクエストターゲットです
if (target[0] !== '/' && target !== '*') {
reply.code(400).send();
return;
}
done();
});
```

アプリケーションの前に配置されたリバースプロキシで、絶対パスのターゲットを拒否または正規化することも同様に有効です。

### 協力

ZeroVuln Labsによって報告されました。

影響を受けるパッケージ:
- npm (JavaScript) @nestjs/platform-fastify: 影響を受けるバージョンは11.2.4未満、11.2.4で修正
- npm (JavaScript) @nestjs/platform-fastify: 影響を受けるバージョンは12.0.0以上、12.0.2未満、12.0.2で修正