mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 04:42:10 +03:00
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:
committed by
GitHub
parent
1ad6c46b46
commit
983e152ea2
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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...");
|
||||
|
||||
96
tests/unit/kiro-region-ssrf-guard.test.ts
Normal file
96
tests/unit/kiro-region-ssrf-guard.test.ts
Normal 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"));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user