GHSA-74w3-63xv-x9mv: Low severity go/github.com/coredns/coredns vulnerability
Summary
A network DNS client allowed to request AXFR for a CoreDNS zone can trigger a panic when CoreDNS is configured with k8sexternal headless-service zone transfers and Kubernetes contains a headless service endpoint with no declared ports. The built-in k8sexternal 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 k8sexternal transferer violates that contract for headless services whose endpoints have no declared ports. plugin/k8sexternal/transfer.go sends the SRV result slice unconditionally:
- plugin/k8sexternal/transfer.go:104-106
go recs, := e.srv(ctx, svcs, s) ch <- recs
plugin/k8sexternal/msgtodns.go intentionally skips services whose port is -1:
- plugin/k8sexternal/msgtodns.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. k8sexternal with headless includes that service in transfer data. 4. k8sexternal.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/k8sexternal/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 k8sexternal transferer emits the empty batch
A temporary package test was added under plugin/k8sexternal:
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/k8sexternal -run TestAuditHeadlessNoPortTransferEmitsEmptyBatch -count=1 -v === RUN TestAuditHeadlessNoPortTransferEmitsEmptyBatch --- PASS: TestAuditHeadlessNoPortTransferEmitsEmptyBatch (0.00s) PASS ok github.com/coredns/coredns/plugin/k8sexternal 0.012s
Additional focused verification:
text $ go test ./plugin/transfer ./plugin/k8sexternal ./plugin/kubernetes -run 'TestAudit|^$' -count=1 -v ok github.com/coredns/coredns/plugin/transfer 0.006s ok github.com/coredns/coredns/plugin/k8sexternal 0.013s ok github.com/coredns/coredns/plugin/kubernetes 0.116s
Affected package suites:
text $ go test ./plugin/transfer ./plugin/k8sexternal ./plugin/kubernetes -count=1 ok github.com/coredns/coredns/plugin/transfer 0.010s ok github.com/coredns/coredns/plugin/k8sexternal 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 k8sexternal 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 Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
go/github.com/coredns/corednsto a version that resolves this vulnerability.Fixed in 1.14.5 - Compensating control
In plugin/k8s_external/transfer.go, send the records slice only when len(recs) > 0 in the headless endpoint and port/protocol transfer paths, preventing empty []dns.RR batches from being emitted.
- Compensating control
In plugin/transfer/transfer.go, skip or reject empty records batches before indexing records[0].
Event History
Frequently Asked Questions
Which deployments are exposed?
Exposure requires CoreDNS to use k8s_external headless-service zone transfers and Kubernetes to contain a headless service endpoint with no declared ports. A network DNS client must also be allowed to request AXFR for the affected zone.
What does an attacker need to do to trigger the issue?
The attacker needs network access to make an AXFR request for an affected CoreDNS zone. No authentication or user interaction is indicated, but the triggering configuration and a headless endpoint without declared ports must be present.
What is the operational impact in normal and debug configurations?
In normal CoreDNS operation, the panic is recovered and returned as SERVFAIL, though repeated requests can disrupt service and increase panic logging. If the debug plugin disables recovery, the same request path can crash the CoreDNS process.
How can operators identify affected conditions?
Review whether k8s_external is configured for headless-service zone transfers and whether any relevant headless service endpoints have no declared ports. Runtime indicators include panic logs and SERVFAIL responses during AXFR handling; a process crash is possible when debug disables recovery.