
Loading

Loading
We use strictly necessary cookies to run this site, and analytics cookies to understand how it's used. See our Privacy Policy for details.
A GitHub-reviewed security advisory, not necessarily under active exploitation the way our CISA KEV advisories are, but a real, disclosed vulnerability in an open-source dependency.
Affected packages
Vulnerable: >= 1.9.4, <= 1.14.4
Patched in 1.14.5
Details
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.
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-131for 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-106recs, _ := 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-112for _, 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-66if 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 converts it to an endpoint with Port: -1.k8s_external with headless includes that service in transfer data.k8s_external.Transfer sends an empty SRV []dns.RR batch.transfer.ServeDNS indexes records[0] and panics.Suggested remediation:
plugin/transfer/transfer.go, skip or reject empty records batches before indexing records[0].plugin/k8s_external/transfer.go, only send recs when len(recs) > 0 in the headless endpoint and port/protocol paths.Port: -1 headless 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
}
...
}
Safe local unit-level reproduction was performed. It does not contact external infrastructure and does not require a live Kubernetes cluster.
transfer sink panics on an empty batchA 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
k8s_external transferer emits the empty batchA 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
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:
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.
EPSS, exploit probability
Low0.46%
estimated chance of real-world exploitation in the next 30 days, higher than 38.4% of every CVE FIRST.org scores
Refreshed 10/16/2026, via FIRST.org's EPSS model, not CVSS, this measures likelihood of exploitation, not how severe it would be.
Talk to us about this
Find out whether this dependency (or others like it) is actually reachable in your codebase.
Get help patching, or building dependency-scanning into your pipeline going forward.
A secure code review or supply chain review finds this before an advisory does.