Trigger.dev: Run replay機能は、攻撃者が指定する環境(テナント間での書き込み)にタスク実行を投入します。

### 概要
ダッシュボードの「リプレイ」機能は、送信元のタスク実行を許可します(これは呼び出し元の組織に属している必要があります)。ただし、リプレイされるタスク実行の対象環境は、リクエストボディからそのまま取得され、組織またはプロジェクトのメンバーシップに関するチェックは一切行われません。リプレイパスで使用される環境の検索機能は、IDのみでフィルタリングを行います。その結果、認証されたユーザーは、自身のタスク実行を別の組織またはプロジェクトの環境にリプレイすることができ、その環境にタスク実行を作成し、被害者のテナントのキューや計算リソースを消費し、実行履歴を汚染する可能性があります。

### 詳細
リプレイ機能は、送信元のタスク実行について、正しく組織のメンバーシップを確認します(apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts、taskRunに対して`project.organization.members.some.userId`でフィルタリングされた`findFirst`)。しかし、対象環境は、送信されたフォームからそのまま渡されます(resources.taskruns.$runParam.replay.ts:344-346):
```js
const replayRunService = new ReplayTaskRunService();
const newRun = await replayRunService.call(taskRun, {
environmentId: submission.value.environment, // 攻撃者が制御可能、検証なし
payload: submission.value.payload,
...
});
```
`submission.value.environment`は、ReplayRunData(apps/webapp/app/v3/replayTask.ts: `environment: z.string().optional()`)から取得されるため、完全にクライアント側で制御されます。

ReplayTaskRunServiceは、所有権のチェックなしでこれを解決します。場所: apps/webapp/app/v3/services/replayTaskRun.server.ts, 26-28行:
```js
const authenticatedEnvironment = await findEnvironmentById(
overrideOptions.environmentId ?? existingTaskRun.runtimeEnvironmentId
);
```
findEnvironmentById, apps/webapp/app/models/runtimeEnvironment.server.ts, 197-211行:
```js
export async function findEnvironmentById(id) {
const environment = await $replica.runtimeEnvironment.findFirst({
where: { id }, // メンバーシップ、組織、プロジェクトのフィルタはなし
include: authIncludeWithParent,
});
if (!environment || environment.project.deletedAt !== null) return null;
return toAuthenticated(environment);
}
```
解決された環境は、TriggerTaskService.call (apps/webapp/app/v3/services/triggerTask.server.ts) に渡され、そこでは渡されたauthenticatedEnvironmentが信頼され、さらなる認証は行われません。リプレイ機能は、環境選択をタスクのプロジェクトに制限していますが (replay.ts, 178-184行)、この制限が再度適用されないため、サーバー側の制御が不足しています(UIからの制限のみ)。

### 実証実験
自身の組織内のタスクの実行者であり、別の組織またはプロジェクトに属するターゲット環境IDを知っている認証済みユーザー(runパラメータ: runParam)として:
```http
POST /resources/taskruns/<own runParam>/replay
Content-Type: application/x-www-form-urlencoded

environment=<被害組織/プロジェクトの環境ID>&failedRedirect=/...
```
リプレイ機能は、メンバーシップのチェックなしで被害側の環境を解決し、それに対してTriggerTaskServiceを呼び出し、被害側のテナント内にタスクの実行を作成します。

ライブ検証:Postgresデータベースに、orgA(env_A)、orgB(env_B_victim)の2つのテナントが設定され、正確なクエリ(リプレイパスで実行されるクエリ)である`findEnvironmentById`(環境をプロジェクトと結合する`findFirst`を使用、`WHERE id = $1`)が、orgAの攻撃者としてenv_B_victimのIDを使用して実行されました。結果:
```
[002 リプレイ findEnvironmentById] 攻撃者(orgA)がenv_B_victimを指定 -> {"id":"env_B_victim","organization_id":"orgB","project_id":"proj_B","deleted_at":null}
クロステナント:確認済み(別の組織の環境を、メンバーシップチェックなしで解決 -> リプレイはそこで実行される)
```
この検索では、別の組織の環境が、メンバーシップのフィルタなしで返されました。クライアントが提供した環境IDが、この検索にそのまま渡され、`TriggerTaskService`が返された環境を信頼しているという点は、ソースコードで検証されています。

### 影響
認証されたユーザーは、別のテナントの環境(本番環境を含む)にタスクの実行を作成し、そのテナントのキューの同時実行数や計算リソースを消費し、実行履歴にエントリを挿入することができます。実用的な攻撃には、ターゲット環境のID(列挙できない)を知っている必要があり、また、挿入されたタスクが実際にエラーにならずに実行されるためには、ターゲット環境に存在するタスク識別子が必要です。これにより、信頼性は制限されますが、不正なクロステナント書き込み自体は制限されません。

### 対策
リプレイアクション(または`ReplayTaskRunService`)において、解決されたターゲット環境が、元のタスク実行のプロジェクト/組織に属していることを必須とします。例えば、`environment.projectId === existingTaskRun.projectId`をアサートする、または、クライアントのメンバーシップに対して提供された環境IDを再検証する(検証済みのプロジェクトIDを使用して`findEnvironmentBySlug`を実行する、または`members.some.userId`フィルタを使用する)などです。これは、既にピッカーに適用されている制約を反映したものです。

影響を受けるパッケージ:
- npm (JavaScript) trigger.dev: 影響あり < 4.5.2、4.5.2で修正済み。