GHSA-74W3-63XV-X9MV
Vulnerability from github – Published: 2026-10-16 19:26 – Updated: 2026-10-16 19:26Summary
A network DNS client allowed to request AXFR for a CoreDNS zone can trigger a panic when CoreDNS is configured with k8s_external headless-service zone transfers and Kubernetes contains a headless service endpoint with no declared ports. The built-in k8s_external transferer can emit an empty []dns.RR batch, and the shared transfer plugin indexes records[0] without checking that the batch is non-empty. In normal CoreDNS mode the panic is recovered and returned as SERVFAIL, but repeated triggering can disrupt requests and amplify panic logging; if the debug plugin disables recovery, the same path can crash the CoreDNS process.
Details
plugin/transfer/transfer.go assumes that every Transferer emits only non-empty []dns.RR batches. During AXFR/IXFR response generation it indexes records[0] without checking the slice length:
plugin/transfer/transfer.go:129-131
for records := range pchan {
if x, ok := records[0].(*dns.SOA); ok && soa == nil {
soa = x
}
The built-in k8s_external transferer violates that contract for headless services whose endpoints have no declared ports. plugin/k8s_external/transfer.go sends the SRV result slice unconditionally:
plugin/k8s_external/transfer.go:104-106
recs, _ := e.srv(ctx, svcs, s)
ch <- recs
plugin/k8s_external/msg_to_dns.go intentionally skips services whose port is -1:
plugin/k8s_external/msg_to_dns.go:107-112
for _, s := range services {
// Don't add the entry if the port is -1 (invalid). The kubernetes plugin uses port -1 when a service/endpoint
// does not have any declared ports.
if s.Port == -1 {
continue
}
The Kubernetes EndpointSlice conversion creates that sentinel when an EndpointSlice has no ports:
plugin/kubernetes/object/endpoint.go:62-66
if len(ends.Ports) == 0 {
// Add sentinel if there are no ports.
e.Subsets[0].Ports = []EndpointPort{{Port: -1}}
This creates a built-in source-to-sink chain:
- Kubernetes EndpointSlice has ready addresses and no ports.
- CoreDNS
kubernetesconverts it to an endpoint withPort: -1. k8s_externalwithheadlessincludes that service in transfer data.k8s_external.Transfersends an empty SRV[]dns.RRbatch.transfer.ServeDNSindexesrecords[0]and panics.
Suggested remediation:
- In
plugin/transfer/transfer.go, skip or reject emptyrecordsbatches before indexingrecords[0]. - In
plugin/k8s_external/transfer.go, only sendrecswhenlen(recs) > 0in the headless endpoint and port/protocol paths. - Add regression tests covering
Port: -1headless services and empty transfer batches.
Example consumer-side guard:
for records := range pchan {
if len(records) == 0 {
continue
}
if x, ok := records[0].(*dns.SOA); ok && soa == nil {
soa = x
}
...
}
PoC
Safe local unit-level reproduction was performed. It does not contact external infrastructure and does not require a live Kubernetes cluster.
Reproducer A: the transfer sink panics on an empty batch
A temporary package test was added under plugin/transfer:
package transfer
import (
"context"
"fmt"
"testing"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/test"
"github.com/miekg/dns"
)
type auditEmptyBatchTransferer struct{}
func (auditEmptyBatchTransferer) Transfer(zone string, serial uint32) (<-chan []dns.RR, error) {
if zone != "example.org." {
return nil, ErrNotAuthoritative
}
ch := make(chan []dns.RR, 3)
ch <- []dns.RR{test.SOA(fmt.Sprintf("%s 100 IN SOA ns.dns.%s hostmaster.%s %d 7200 1800 86400 100", zone, zone, zone, 1))}
ch <- []dns.RR{}
close(ch)
return ch, nil
}
func TestAuditEmptyTransferBatchPanics(t *testing.T) {
tr := &Transfer{
Transferers: []Transferer{auditEmptyBatchTransferer{}},
xfrs: []*xfr{{Zones: []string{"example.org."}, to: []string{"*"}}},
}
req := new(dns.Msg)
req.SetQuestion("example.org.", dns.TypeAXFR)
rec := dnstest.NewRecorder(&test.ResponseWriter{TCP: true})
defer func() {
if rec := recover(); rec == nil {
t.Fatal("expected empty transfer batch to panic")
}
}()
_, _ = tr.ServeDNS(context.Background(), rec, req)
}
Verification command and result:
$ go test ./plugin/transfer -run TestAuditEmptyTransferBatchPanics -count=1 -v
=== RUN TestAuditEmptyTransferBatchPanics
--- PASS: TestAuditEmptyTransferBatchPanics (0.00s)
PASS
ok github.com/coredns/coredns/plugin/transfer 0.005s
Reproducer B: the built-in k8s_external transferer emits the empty batch
A temporary package test was added under plugin/k8s_external:
package external
import (
"testing"
"time"
"github.com/coredns/coredns/plugin/etcd/msg"
"github.com/coredns/coredns/plugin/kubernetes"
"github.com/coredns/coredns/request"
"github.com/miekg/dns"
)
func TestAuditHeadlessNoPortTransferEmitsEmptyBatch(t *testing.T) {
e := New()
e.Zones = []string{"example.com."}
e.headless = true
e.externalAddrFunc = func(request.Request, bool) []dns.RR { return nil }
e.externalSerialFunc = func(string) uint32 { return 1 }
e.externalServicesFunc = func(string, bool) ([]msg.Service, map[string][]msg.Service) {
return nil, map[string][]msg.Service{
"/coredns/com/example/testns/headless/" + kubernetes.Endpoint: {{Host: "192.0.2.10", Port: -1, TTL: 5, Key: "/coredns/com/example/testns/headless/endpoint-a"}},
}
}
ch, err := e.Transfer("example.com.", 0)
if err != nil {
t.Fatalf("Transfer returned error: %v", err)
}
deadline := time.After(time.Second)
for {
select {
case batch, ok := <-ch:
if !ok {
t.Fatal("transfer closed before emitting an empty batch")
}
if len(batch) == 0 {
return
}
case <-deadline:
t.Fatal("timed out waiting for an empty batch")
}
}
}
Verification command and result:
$ go test ./plugin/k8s_external -run TestAuditHeadlessNoPortTransferEmitsEmptyBatch -count=1 -v
=== RUN TestAuditHeadlessNoPortTransferEmitsEmptyBatch
--- PASS: TestAuditHeadlessNoPortTransferEmitsEmptyBatch (0.00s)
PASS
ok github.com/coredns/coredns/plugin/k8s_external 0.012s
Additional focused verification:
$ go test ./plugin/transfer ./plugin/k8s_external ./plugin/kubernetes -run 'TestAudit|^$' -count=1 -v
ok github.com/coredns/coredns/plugin/transfer 0.006s
ok github.com/coredns/coredns/plugin/k8s_external 0.013s
ok github.com/coredns/coredns/plugin/kubernetes 0.116s
Affected package suites:
$ go test ./plugin/transfer ./plugin/k8s_external ./plugin/kubernetes -count=1
ok github.com/coredns/coredns/plugin/transfer 0.010s
ok github.com/coredns/coredns/plugin/k8s_external 0.011s
ok github.com/coredns/coredns/plugin/kubernetes 0.194s
Impact
A network client allowed to request AXFR for the affected zone can trigger a panic in CoreDNS when all prerequisites are present.
Required configuration/state:
example.com:53 {
kubernetes cluster.local
k8s_external example.com {
headless
}
transfer example.com {
to *
}
}
Additional prerequisite:
- The Kubernetes API state contains a headless-service endpoint represented by an EndpointSlice with ready addresses and no ports, producing CoreDNS
msg.Service{Port: -1}.
In normal CoreDNS mode, core/dnsserver/server.go:267-279 recovers the panic and returns SERVFAIL, so this is a request-level availability issue and panic/log amplification issue. If the debug plugin is enabled, the server disables the recovery path and this can crash the CoreDNS process.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.14.4"
},
"package": {
"ecosystem": "Go",
"name": "github.com/coredns/coredns"
},
"ranges": [
{
"events": [
{
"introduced": "1.9.4"
},
{
"fixed": "1.14.5"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-62994"
],
"database_specific": {
"cwe_ids": [
"CWE-248",
"CWE-755"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-16T19:26:42Z",
"nvd_published_at": "2026-07-16T20:16:47Z",
"severity": "LOW"
},
"details": "### Summary\n\nA network DNS client allowed to request AXFR for a CoreDNS zone can trigger a panic when CoreDNS is configured with `k8s_external` headless-service zone transfers and Kubernetes contains a headless service endpoint with no declared ports. The built-in `k8s_external` transferer can emit an empty `[]dns.RR` batch, and the shared `transfer` plugin indexes `records[0]` without checking that the batch is non-empty. In normal CoreDNS mode the panic is recovered and returned as `SERVFAIL`, but repeated triggering can disrupt requests and amplify panic logging; if the `debug` plugin disables recovery, the same path can crash the CoreDNS process.\n\n### Details\n\n`plugin/transfer/transfer.go` assumes that every `Transferer` emits only non-empty `[]dns.RR` batches. During AXFR/IXFR response generation it indexes `records[0]` without checking the slice length:\n\n- `plugin/transfer/transfer.go:129-131`\n\n```go\nfor records := range pchan {\n if x, ok := records[0].(*dns.SOA); ok \u0026\u0026 soa == nil {\n soa = x\n }\n```\n\nThe built-in `k8s_external` transferer violates that contract for headless services whose endpoints have no declared ports. `plugin/k8s_external/transfer.go` sends the SRV result slice unconditionally:\n\n- `plugin/k8s_external/transfer.go:104-106`\n\n```go\nrecs, _ := e.srv(ctx, svcs, s)\nch \u003c- recs\n```\n\n`plugin/k8s_external/msg_to_dns.go` intentionally skips services whose port is `-1`:\n\n- `plugin/k8s_external/msg_to_dns.go:107-112`\n\n```go\nfor _, s := range services {\n // Don\u0027t add the entry if the port is -1 (invalid). The kubernetes plugin uses port -1 when a service/endpoint\n // does not have any declared ports.\n if s.Port == -1 {\n continue\n }\n```\n\nThe Kubernetes EndpointSlice conversion creates that sentinel when an EndpointSlice has no ports:\n\n- `plugin/kubernetes/object/endpoint.go:62-66`\n\n```go\nif len(ends.Ports) == 0 {\n // Add sentinel if there are no ports.\n e.Subsets[0].Ports = []EndpointPort{{Port: -1}}\n```\n\nThis creates a built-in source-to-sink chain:\n\n1. Kubernetes EndpointSlice has ready addresses and no ports.\n2. CoreDNS `kubernetes` converts it to an endpoint with `Port: -1`.\n3. `k8s_external` with `headless` includes that service in transfer data.\n4. `k8s_external.Transfer` sends an empty SRV `[]dns.RR` batch.\n5. `transfer.ServeDNS` indexes `records[0]` and panics.\n\nSuggested remediation:\n\n1. In `plugin/transfer/transfer.go`, skip or reject empty `records` batches before indexing `records[0]`.\n2. In `plugin/k8s_external/transfer.go`, only send `recs` when `len(recs) \u003e 0` in the headless endpoint and port/protocol paths.\n3. Add regression tests covering `Port: -1` headless services and empty transfer batches.\n\nExample consumer-side guard:\n\n```go\nfor records := range pchan {\n if len(records) == 0 {\n continue\n }\n if x, ok := records[0].(*dns.SOA); ok \u0026\u0026 soa == nil {\n soa = x\n }\n ...\n}\n```\n\n### PoC\n\nSafe local unit-level reproduction was performed. It does not contact external infrastructure and does not require a live Kubernetes cluster.\n\n#### Reproducer A: the `transfer` sink panics on an empty batch\n\nA temporary package test was added under `plugin/transfer`:\n\n```go\npackage transfer\n\nimport (\n \"context\"\n \"fmt\"\n \"testing\"\n\n \"github.com/coredns/coredns/plugin/pkg/dnstest\"\n \"github.com/coredns/coredns/plugin/test\"\n\n \"github.com/miekg/dns\"\n)\n\ntype auditEmptyBatchTransferer struct{}\n\nfunc (auditEmptyBatchTransferer) Transfer(zone string, serial uint32) (\u003c-chan []dns.RR, error) {\n if zone != \"example.org.\" {\n return nil, ErrNotAuthoritative\n }\n ch := make(chan []dns.RR, 3)\n ch \u003c- []dns.RR{test.SOA(fmt.Sprintf(\"%s 100 IN SOA ns.dns.%s hostmaster.%s %d 7200 1800 86400 100\", zone, zone, zone, 1))}\n ch \u003c- []dns.RR{}\n close(ch)\n return ch, nil\n}\n\nfunc TestAuditEmptyTransferBatchPanics(t *testing.T) {\n tr := \u0026Transfer{\n Transferers: []Transferer{auditEmptyBatchTransferer{}},\n xfrs: []*xfr{{Zones: []string{\"example.org.\"}, to: []string{\"*\"}}},\n }\n\n req := new(dns.Msg)\n req.SetQuestion(\"example.org.\", dns.TypeAXFR)\n rec := dnstest.NewRecorder(\u0026test.ResponseWriter{TCP: true})\n\n defer func() {\n if rec := recover(); rec == nil {\n t.Fatal(\"expected empty transfer batch to panic\")\n }\n }()\n\n _, _ = tr.ServeDNS(context.Background(), rec, req)\n}\n```\n\nVerification command and result:\n\n```text\n$ go test ./plugin/transfer -run TestAuditEmptyTransferBatchPanics -count=1 -v\n=== RUN TestAuditEmptyTransferBatchPanics\n--- PASS: TestAuditEmptyTransferBatchPanics (0.00s)\nPASS\nok \tgithub.com/coredns/coredns/plugin/transfer\t0.005s\n```\n\n#### Reproducer B: the built-in `k8s_external` transferer emits the empty batch\n\nA temporary package test was added under `plugin/k8s_external`:\n\n```go\npackage external\n\nimport (\n \"testing\"\n \"time\"\n\n \"github.com/coredns/coredns/plugin/etcd/msg\"\n \"github.com/coredns/coredns/plugin/kubernetes\"\n \"github.com/coredns/coredns/request\"\n\n \"github.com/miekg/dns\"\n)\n\nfunc TestAuditHeadlessNoPortTransferEmitsEmptyBatch(t *testing.T) {\n e := New()\n e.Zones = []string{\"example.com.\"}\n e.headless = true\n e.externalAddrFunc = func(request.Request, bool) []dns.RR { return nil }\n e.externalSerialFunc = func(string) uint32 { return 1 }\n e.externalServicesFunc = func(string, bool) ([]msg.Service, map[string][]msg.Service) {\n return nil, map[string][]msg.Service{\n \"/coredns/com/example/testns/headless/\" + kubernetes.Endpoint: {{Host: \"192.0.2.10\", Port: -1, TTL: 5, Key: \"/coredns/com/example/testns/headless/endpoint-a\"}},\n }\n }\n\n ch, err := e.Transfer(\"example.com.\", 0)\n if err != nil {\n t.Fatalf(\"Transfer returned error: %v\", err)\n }\n\n deadline := time.After(time.Second)\n for {\n select {\n case batch, ok := \u003c-ch:\n if !ok {\n t.Fatal(\"transfer closed before emitting an empty batch\")\n }\n if len(batch) == 0 {\n return\n }\n case \u003c-deadline:\n t.Fatal(\"timed out waiting for an empty batch\")\n }\n }\n}\n```\n\nVerification command and result:\n\n```text\n$ go test ./plugin/k8s_external -run TestAuditHeadlessNoPortTransferEmitsEmptyBatch -count=1 -v\n=== RUN TestAuditHeadlessNoPortTransferEmitsEmptyBatch\n--- PASS: TestAuditHeadlessNoPortTransferEmitsEmptyBatch (0.00s)\nPASS\nok \tgithub.com/coredns/coredns/plugin/k8s_external\t0.012s\n```\n\nAdditional focused verification:\n\n```text\n$ go test ./plugin/transfer ./plugin/k8s_external ./plugin/kubernetes -run \u0027TestAudit|^$\u0027 -count=1 -v\nok \tgithub.com/coredns/coredns/plugin/transfer\t0.006s\nok \tgithub.com/coredns/coredns/plugin/k8s_external\t0.013s\nok \tgithub.com/coredns/coredns/plugin/kubernetes\t0.116s\n```\n\nAffected package suites:\n\n```text\n$ go test ./plugin/transfer ./plugin/k8s_external ./plugin/kubernetes -count=1\nok \tgithub.com/coredns/coredns/plugin/transfer\t0.010s\nok \tgithub.com/coredns/coredns/plugin/k8s_external\t0.011s\nok \tgithub.com/coredns/coredns/plugin/kubernetes\t0.194s\n```\n\n### Impact\n\nA network client allowed to request AXFR for the affected zone can trigger a panic in CoreDNS when all prerequisites are present.\n\nRequired configuration/state:\n\n```corefile\nexample.com:53 {\n kubernetes cluster.local\n k8s_external example.com {\n headless\n }\n transfer example.com {\n to *\n }\n}\n```\n\nAdditional prerequisite:\n\n- The Kubernetes API state contains a headless-service endpoint represented by an EndpointSlice with ready addresses and no ports, producing CoreDNS `msg.Service{Port: -1}`.\n\nIn normal CoreDNS mode, `core/dnsserver/server.go:267-279` recovers the panic and returns `SERVFAIL`, so this is a request-level availability issue and panic/log amplification issue. If the `debug` plugin is enabled, the server disables the recovery path and this can crash the CoreDNS process.",
"id": "GHSA-74w3-63xv-x9mv",
"modified": "2026-10-16T19:26:42Z",
"published": "2026-10-16T19:26:42Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/coredns/coredns/security/advisories/GHSA-74w3-63xv-x9mv"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-62994"
},
{
"type": "WEB",
"url": "https://github.com/coredns/coredns/pull/8207"
},
{
"type": "WEB",
"url": "https://github.com/coredns/coredns/commit/ab318db7b4a3a19273852ed627f54888198c8efb"
},
{
"type": "PACKAGE",
"url": "https://github.com/coredns/coredns"
},
{
"type": "WEB",
"url": "https://github.com/coredns/coredns/releases/tag/v1.14.5"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L",
"type": "CVSS_V3"
}
],
"summary": "CoreDNS `k8s_external` headless AXFR can emit an empty transfer batch that panics the `transfer` plugin"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
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.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
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.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.