---
id: CVE-2026-56676
aliases:
  - GHSA-cmhj-wh2f-9cgx
title: '9router: Image prefetch DNS rebinding allows SSRF to internal services'
summary: '9router: Image prefetch DNS rebinding allows SSRF to internal services'
severity: high
cvss: 7.4
cwe:
  - CWE-367
  - CWE-918
vendor: 9router
product: 9router
ecosystem: npm
affected:
  - 9router <= 0.4.80
patched:
  - 9router 0.5.2
published: '2026-09-23'
updated: '2026-09-23'
sourceUpdated: '2026-09-23T18:12:29Z'
source: GHSA
sourceUrl: 'https://github.com/advisories/GHSA-cmhj-wh2f-9cgx'
references:
  - url: 'https://github.com/decolua/9router/security/advisories/GHSA-cmhj-wh2f-9cgx'
  - url: 'https://nvd.nist.gov/vuln/detail/CVE-2026-56676'
  - url: >-
      https://github.com/decolua/9router/commit/c7d07448c58bec1200741de0b73305b860416b82
  - url: 'https://github.com/decolua/9router/releases/tag/v0.5.2'
  - url: 'https://github.com/advisories/GHSA-cmhj-wh2f-9cgx'
tags:
  - ghsa
  - npm
epss: 0.0026
epssPercentile: 0.15717
ingestedAt: '2026-09-23T18:29:33.151Z'
---

## Overview

## Summary

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/completions` with a **vision-capable model** and an
  `image_url` content 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-marker` with `peer=172.29.0.30` (the `proxied-router`
  container), proving the server-side fetch landed on the internal service.
- **mock-provider** receives `POST /api/chat` and the flow completes with `HTTP 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.

1. Build and start the stack:
   ```bash
   docker compose up --build
   ```
2. Confirm `internal-admin` is 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
   ```
3. Send the request named **`POST image-prefetch DNS rebinding trigger`** from
   [`requests.http`](./requests.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 packages

- `9router <= 0.4.80`

## Remediation

Upgrade to a patched release:

- `9router 0.5.2`
