Answer in brief
CVE-2026-62994 records a Low severity (CVSS 3.7) vulnerability in CoreDNS `k8s_external` headless AXFR can emit an empty transfer batch that panics the `transfer` plugin. The current sources do not mark it as known exploited. The current feed maps github.com/coredns/coredns (go). Check affected ranges and fixed versions before updating.
Analysis pending evidence review
HOL Guard separates source facts from reviewed analysis. See the methodology.
CVSS is 3.7. The current sources do not mark it as known exploited. Treat this as a source-backed prioritization signal, not a statement about your environment.
Analysis status
Analysis pending evidence review
Factual feed record only; HOL analysis is not approved for indexing. Read the methodology.
The current feed maps github.com/coredns/coredns (go). Check affected ranges and fixed versions before updating.
| Package | Affected range | Fixed version |
|---|---|---|
| github.com/coredns/corednsgo | >=1.9.4,<=1.14.4 | 1.14.5 |
Published upstream
Jul 16, 2026
Evidence: source:ghsa:source_dates:source-dates:recordSource modified
Oct 16, 2026
Evidence: source:ghsa:source_dates:source-dates:recordFirst seen by HOL
Jul 17, 2026
### Summary 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` ```go 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` ```go 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` ```go 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` ```go 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: 1. Kubernetes EndpointSlice has ready addresses and no ports. 2. CoreDNS `kubernetes` converts it to an endpoint with `Port: -1`. 3. `k8s_external` with `headless` includes that service in transfer data. 4. `k8s_external.Transfer` sends an empty SRV `[]dns.RR` batch. 5. `transfer.ServeDNS` indexes `records[0]` and panics. Suggested remediation: 1. In `plugin/transfer/transfer.go`, skip or reject empty `records` batches before indexing `records[0]`. 2. In `plugin/k8s_external/transfer.go`, only send `recs` when `len(recs) > 0` in the headless endpoint and port/protocol paths. 3. Add regression tests covering `Port: -1` headless services and empty transfer batches. Example consumer-side guard: ```go 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`: ```go 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: ```text $ 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`: ```go 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: ```text $ 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: ```text $ 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: ```text $ 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: ```corefile 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.
Quoted source text, attributed separately from HOL analysis.