fix(security): validate kiro region to prevent SSRF (GHSA-6mwv-4mrm-5p3m) (#4629)

Integrated into release/v3.8.36 — kiro region SSRF guard (GHSA-6mwv-4mrm-5p3m), port rebuilt clean over release tip
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-23 21:52:21 -03:00
committed by GitHub
parent 1ad6c46b46
commit 983e152ea2
4 changed files with 129 additions and 3 deletions

View File

@@ -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)

View File

@@ -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"

View File

@@ -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...");

View File

@@ -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"));
});
});