From 983e152ea27d001e19f8ea18b16b2c49100b21f1 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Tue, 23 Jun 2026 21:52:21 -0300 Subject: [PATCH] fix(security): validate kiro region to prevent SSRF (GHSA-6mwv-4mrm-5p3m) (#4629) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrated into release/v3.8.36 — kiro region SSRF guard (GHSA-6mwv-4mrm-5p3m), port rebuilt clean over release tip --- src/lib/oauth/constants/oauth.ts | 16 ++++ src/lib/oauth/providers/kiro.ts | 13 ++- src/lib/oauth/services/kiro.ts | 7 +- tests/unit/kiro-region-ssrf-guard.test.ts | 96 +++++++++++++++++++++++ 4 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 tests/unit/kiro-region-ssrf-guard.test.ts diff --git a/src/lib/oauth/constants/oauth.ts b/src/lib/oauth/constants/oauth.ts index f415963fa6..4d33ce4229 100644 --- a/src/lib/oauth/constants/oauth.ts +++ b/src/lib/oauth/constants/oauth.ts @@ -262,6 +262,22 @@ export const GITLAB_DUO_CONFIG = { codeChallengeMethod: "S256", }; +// AWS region allowlist — prevents SSRF via region injection into upstream URLs +// (GHSA-6mwv-4mrm-5p3m). Region values flow user-supplied through the kiro OAuth +// import surfaces (request body, providerSpecificData) and are interpolated into +// URLs like `https://oidc.${region}.amazonaws.com/...`. Without this guard, a +// region like "127.0.0.1" or "evil.com" would redirect the proxy's outbound +// fetch to an attacker-controlled host. Canonical AWS region shape only: +// two letters, dash, one-or-more letters, dash, one-or-two digits. +export const AWS_REGION_PATTERN = /^[a-z]{2}-[a-z]+-\d{1,2}$/; + +export function assertValidAwsRegion(region: string): string { + if (typeof region !== "string" || !AWS_REGION_PATTERN.test(region)) { + throw new Error("Invalid region"); + } + return region; +} + // Kiro OAuth Configuration // Supports multiple auth methods: // 1. AWS Builder ID (Device Code Flow) diff --git a/src/lib/oauth/providers/kiro.ts b/src/lib/oauth/providers/kiro.ts index 63d9965114..2efb7fc609 100644 --- a/src/lib/oauth/providers/kiro.ts +++ b/src/lib/oauth/providers/kiro.ts @@ -1,11 +1,16 @@ -import { KIRO_CONFIG } from "../constants/oauth"; +import { KIRO_CONFIG, AWS_REGION_PATTERN, assertValidAwsRegion } from "../constants/oauth"; export const kiro = { config: KIRO_CONFIG, flowType: "device_code", requestDeviceCode: async (config) => { const regionMatch = String(config.tokenUrl || "").match(/oidc\.([a-z0-9-]+)\.amazonaws\.com/i); - const resolvedRegion = regionMatch?.[1] || "us-east-1"; + const candidateRegion = regionMatch?.[1] || "us-east-1"; + // Region is sourced from KIRO_CONFIG.tokenUrl (trusted constant) but defensively + // re-validate before letting it influence later fetches (GHSA-6mwv-4mrm-5p3m). + const resolvedRegion = AWS_REGION_PATTERN.test(candidateRegion) + ? candidateRegion + : "us-east-1"; const registerPayload: { clientName: string; clientType: string; @@ -77,6 +82,7 @@ export const kiro = { }, pollToken: async (config, deviceCode, codeVerifier, extraData) => { const tokenRegion = String(extraData?._region || "us-east-1").toLowerCase(); + assertValidAwsRegion(tokenRegion); const tokenUrl = `https://oidc.${tokenRegion}.amazonaws.com/token`; const response = await fetch(tokenUrl, { @@ -132,6 +138,9 @@ export const kiro = { const accessToken = tokenData?.access_token; if (!accessToken) return null; const region = String(tokenData?._region || "us-east-1").toLowerCase(); + // Defensive: tokenData._region came from upstream JSON or extraData + // and is interpolated into the runtime host below (GHSA-6mwv-4mrm-5p3m). + if (!AWS_REGION_PATTERN.test(region)) return null; const runtimeHost = region === "us-east-1" ? "https://codewhisperer.us-east-1.amazonaws.com" diff --git a/src/lib/oauth/services/kiro.ts b/src/lib/oauth/services/kiro.ts index e3ca235de3..b1357533f3 100644 --- a/src/lib/oauth/services/kiro.ts +++ b/src/lib/oauth/services/kiro.ts @@ -1,4 +1,4 @@ -import { KIRO_CONFIG } from "../constants/oauth"; +import { KIRO_CONFIG, assertValidAwsRegion } from "../constants/oauth"; /** * Kiro OAuth Service @@ -17,6 +17,7 @@ export class KiroService { * Returns clientId and clientSecret for device code flow */ async registerClient(region: string = "us-east-1") { + assertValidAwsRegion(region); const endpoint = `https://oidc.${region}.amazonaws.com/client/register`; const response = await fetch(endpoint, { @@ -55,6 +56,7 @@ export class KiroService { startUrl: string, region: string = "us-east-1" ) { + assertValidAwsRegion(region); const endpoint = `https://oidc.${region}.amazonaws.com/device_authorization`; const response = await fetch(endpoint, { @@ -94,6 +96,7 @@ export class KiroService { deviceCode: string, region: string = "us-east-1" ) { + assertValidAwsRegion(region); const endpoint = `https://oidc.${region}.amazonaws.com/token`; const response = await fetch(endpoint, { @@ -189,6 +192,7 @@ export class KiroService { // but a Kiro-social refresh token the OIDC client can't refresh — use the social path (#2467). if (clientId && clientSecret && authMethod !== "imported") { const resolvedRegion = region || "us-east-1"; + assertValidAwsRegion(resolvedRegion); const endpoint = `https://oidc.${resolvedRegion}.amazonaws.com/token`; const response = await fetch(endpoint, { @@ -288,6 +292,7 @@ export class KiroService { * If registerClient() also fails, the import falls back to the shared social-auth refresh path. */ async validateImportToken(refreshToken: string, region: string = "us-east-1") { + assertValidAwsRegion(region); // Validate token format if (!refreshToken.startsWith("aorAAAAAG")) { throw new Error("Invalid token format. Token should start with aorAAAAAG..."); diff --git a/tests/unit/kiro-region-ssrf-guard.test.ts b/tests/unit/kiro-region-ssrf-guard.test.ts new file mode 100644 index 0000000000..57adc8015a --- /dev/null +++ b/tests/unit/kiro-region-ssrf-guard.test.ts @@ -0,0 +1,96 @@ +// SSRF regression guard for kiro region — GHSA-6mwv-4mrm-5p3m. +// Without the assertValidAwsRegion guard, a malicious region value +// would be interpolated directly into upstream URLs like +// `https://oidc.${region}.amazonaws.com/token`, allowing the caller to +// redirect the proxy to arbitrary hosts (file://, 127.0.0.1, EC2 metadata, etc). +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { + AWS_REGION_PATTERN, + assertValidAwsRegion, +} from "../../src/lib/oauth/constants/oauth"; +import { KiroService } from "../../src/lib/oauth/services/kiro"; + +describe("AWS_REGION_PATTERN", () => { + it("accepts canonical AWS regions", () => { + for (const r of [ + "us-east-1", + "us-west-2", + "eu-west-1", + "ap-southeast-2", + "ca-central-1", + "sa-east-1", + "me-south-1", + "af-south-1", + "eu-central-2", + ]) { + assert.ok(AWS_REGION_PATTERN.test(r), `expected ${r} to match`); + } + }); + + it("rejects SSRF-shaped values", () => { + for (const bad of [ + "127.0.0.1", + "169.254.169.254", + "localhost", + "evil.example.com", + "us-east-1.evil.com", + "us-east-1/../foo", + "us-east-1#frag", + "us-east-1?x=1", + "file:///etc/passwd", + "http://internal", + "", + "US-EAST-1", // wrong case + "us_east_1", + "us-east-", + "-east-1", + "us--east-1", + ]) { + assert.ok(!AWS_REGION_PATTERN.test(bad), `expected ${bad!} to be rejected`); + } + }); +}); + +describe("assertValidAwsRegion", () => { + it("returns the region when valid", () => { + assert.equal(assertValidAwsRegion("us-east-1"), "us-east-1"); + }); + + it("throws on non-string", () => { + assert.throws(() => assertValidAwsRegion(undefined as unknown as string)); + assert.throws(() => assertValidAwsRegion(null as unknown as string)); + assert.throws(() => assertValidAwsRegion(123 as unknown as string)); + }); + + it("throws on invalid region", () => { + assert.throws(() => assertValidAwsRegion("127.0.0.1")); + assert.throws(() => assertValidAwsRegion("evil.com")); + assert.throws(() => assertValidAwsRegion("")); + }); +}); + +describe("KiroService SSRF guard", () => { + const svc = new KiroService(); + + it("registerClient rejects malicious region", async () => { + await assert.rejects(() => svc.registerClient("127.0.0.1")); + await assert.rejects(() => svc.registerClient("evil.example.com")); + }); + + it("startDeviceAuthorization rejects malicious region", async () => { + await assert.rejects(() => + svc.startDeviceAuthorization("cid", "csec", "https://x", "169.254.169.254") + ); + }); + + it("pollDeviceToken rejects malicious region", async () => { + await assert.rejects(() => + svc.pollDeviceToken("cid", "csec", "dc", "127.0.0.1") + ); + }); + + it("validateImportToken rejects malicious region before fetching", async () => { + await assert.rejects(() => svc.validateImportToken("aorAAAAAGfoo", "127.0.0.1")); + }); +});