From 5d1055f3fcffdbb3bde5a3f6078aa1d77e1a190b Mon Sep 17 00:00:00 2001 From: Xiangzhe Date: Fri, 21 Aug 2026 14:12:55 -0300 Subject: [PATCH] fix(security): validate the AWS region on the device-code route (SSRF) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kiro / amazon-q device-code action interpolated a caller-supplied `region` into the AWS OIDC endpoint URLs that requestDeviceCode() fetches, with no validation — an attacker-shaped region (userinfo/fragment) could re-point the outbound host to an internal target or the cloud-metadata service. `region` is now checked against the canonical AWS region shape (AWS_REGION_PATTERN, already used by pollToken) and rejected with a 400 before any outbound fetch. Reported by @daniel-mertz via GHSA-7x63-xvp5-w2jc. --- .../api/oauth/[provider]/[action]/route.ts | 11 ++++ .../oauth-device-code-region-ssrf.test.ts | 54 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 tests/unit/oauth-device-code-region-ssrf.test.ts diff --git a/src/app/api/oauth/[provider]/[action]/route.ts b/src/app/api/oauth/[provider]/[action]/route.ts index 7b71a07758..b52bae4201 100755 --- a/src/app/api/oauth/[provider]/[action]/route.ts +++ b/src/app/api/oauth/[provider]/[action]/route.ts @@ -24,6 +24,7 @@ import { } from "@/models"; import { getConsistentMachineId } from "@/shared/utils/machineId"; import { isValidGheUrl } from "@/shared/validation/providerSpecificData"; +import { AWS_REGION_PATTERN } from "@/lib/oauth/constants/oauth"; import { syncToCloud } from "@/lib/cloudSync"; import { startLocalServer } from "@/lib/oauth/utils/server"; import { runWithProxyContextOrDirect } from "@omniroute/open-sse/utils/proxyFetch.ts"; @@ -221,6 +222,16 @@ export async function GET( (requestDeviceCode as any)(provider, null, providerOverrideConfig) ); } else if ((provider === "kiro" || provider === "amazon-q") && startUrl) { + // GHSA-7x63: `region` is interpolated into the AWS OIDC endpoint URLs + // below, which requestDeviceCode() then fetches. Validate it against the + // canonical AWS region shape before it can steer the outbound host to an + // attacker-chosen target (userinfo/fragment tricks → SSRF / metadata). + if (!AWS_REGION_PATTERN.test(region)) { + return NextResponse.json( + { error: "region must be a valid AWS region (e.g. us-east-1)" }, + { status: 400 } + ); + } const providerOverrideConfig = { ...providerData.config, startUrl, diff --git a/tests/unit/oauth-device-code-region-ssrf.test.ts b/tests/unit/oauth-device-code-region-ssrf.test.ts new file mode 100644 index 0000000000..bc57659f79 --- /dev/null +++ b/tests/unit/oauth-device-code-region-ssrf.test.ts @@ -0,0 +1,54 @@ +/** + * GHSA-7x63-xvp5-w2jc — the kiro / amazon-q device-code action interpolates a + * caller-supplied `region` into the AWS OIDC endpoint URLs that requestDeviceCode() + * fetches. An attacker-shaped region (userinfo / fragment) re-points the outbound + * host (SSRF → cloud metadata). The route must reject a non-canonical region with + * a 400 before any outbound fetch. + */ + +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import type { NextRequest } from "next/server"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omni-oauth-region-ssrf-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const route = await import("../../src/app/api/oauth/[provider]/[action]/route.ts"); + +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +async function deviceCode(provider: string, region: string) { + const url = + `http://localhost/api/oauth/${provider}/device-code` + + `?startUrl=${encodeURIComponent("https://d-1234567890.awsapps.com/start")}` + + `®ion=${encodeURIComponent(region)}`; + return route.GET(new Request(url) as unknown as NextRequest, { + params: Promise.resolve({ provider, action: "device-code" }), + }); +} + +test("kiro device-code rejects a non-canonical region before any outbound fetch (GHSA-7x63)", async () => { + for (const bad of [ + "evil.com", + "169.254.169.254", + "us-east-1@169.254.169.254", + "us-east-1#.amazonaws.com@evil.com", + "us-east-1/../..", + "US-EAST-1", // uppercase is not the canonical shape + ]) { + const res = await deviceCode("kiro", bad); + assert.equal(res.status, 400, `region "${bad}" must be rejected with 400`); + } +}); + +test("amazon-q device-code also validates region", async () => { + const res = await deviceCode("amazon-q", "evil.com:1@169.254.169.254"); + assert.equal(res.status, 400); +});