# 9router: Image prefetch DNS rebinding allows SSRF to internal services

9router's image prefetch validated host DNS then re-resolved at fetch, letting a rebinding domain reach internal-only HTTP services via SSRF.

- Payout: Research
- Severity: high
- Program: 9router
- Found by: dinhvaren
- Disclosed: 2026-09-23
- CVE: CVE-2026-56676
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-cmhj-wh2f-9cgx
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-cmhj-wh2f-9cgx
- Tags: area:cloud domain:infra domain:web impact:internal-access kind:research platform:ghsa sev:high tech:docker tech:llm technique:race-window vuln:race-condition vuln:ssrf

**Target:** Server-side image prefetch in /v1/chat/completions (vision image_url)
**Root cause:** The public-host guard resolves the image hostname, but the subsequent server-side fetch performs an independent DNS resolution; the IP is never pinned between check and use, so a TTL-0 rebinding authority returns a public IP to the guard and an internal IP to the fetch.
**Steps:**
1. `docker compose up --build` to start the stack (router, mock-provider, internal-admin, rebind-dns).
2. Confirm internal-admin is not host-reachable: `curl -i http://127.0.0.1:18083/ssrf-marker` fails and `docker compose ps` shows no host port mapping.
3. POST to `http://127.0.0.1:18082/v1/chat/completions` with `"model": "ollama-local/gemma3"` and a content part `{"type":"image_url","image_url":{"url":"http://rebind.9r.test:8080/ssrf-marker?case=rebind-trigger"}}`, `"stream": false`.
4. rebind.9r.test first answers A=1.1.1.1 (passes guard), then A=172.29.0.10 on the fetch resolution.
5. internal-admin logs `GET /ssrf-marker` with `peer=172.29.0.30` (proxied-router container); mock-provider receives `POST /api/chat` and flow returns HTTP 200.
**How found:** Noticed the validation step and the fetch each perform their own DNS resolution rather than pinning the checked IP, making the public-host guard a classic TOCTOU; built a Docker Compose repro with a rebinding DNS authority and an internal-only admin service to prove the fetch lands internally.
**Exploitation:** Attacker submits a chat completion request with a vision-capable model and an image_url pointing at an attacker-controlled TTL-0 DNS name. The guard resolves it to a public IP; the fetch re-resolves to an internal Docker IP, so the server issues the GET to internal-only services. Blind/semi-blind by default; an exfil variant points the image at an internal endpoint returning valid image bytes so content is base64-encoded back to the upstream.
**Escalation:** Exfil variant: target internal endpoints that return valid image bytes so the fetched content is returned base64-encoded to the attacker; otherwise use for internal service discovery and reaching cloud metadata/admin panels.
**Insight:** When a guard resolves a hostname and the fetch resolves it again, a TTL-0 rebinding authority defeats the check; test image/URL prefetch paths with a two-answer DNS server and confirm the internal hit via peer IP in target logs.

