Track vulnerabilities like this one. Sign up free to get alerted when software you use is affected.
3.7

CVE-2026-62994: CoreDNS can crash when handling certain DNS requests

CVE-2026-62994 · published 2 days ago
Summary

A DNS client can crash the CoreDNS server if it requests a specific type of transfer. This can happen if you're using a certain type of DNS zone transfer with CoreDNS. To fix this, update to CoreDNS version 1.14.5 or later.

What to do
  • Update github.com coredns to version 1.14.5.
Affected software
Ecosystem VendorProductAffected versions
– coredns coredns >= 1.9.4, < 1.14.5
go github.com coredns >= 1.9.4, <= 1.14.4
Fix: upgrade to 1.14.5
Original advisory text
CoreDNS `k8s_external` headless AXFR can emit an empty transfer batch that panics the `transfer` plugin
### 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.
Fix within
Internet-facing 60 days
Internal At next upgrade
  • Not known to be exploited
  • Needs hands-on effort to exploit
  • Gives an attacker partial control
Severity
3.7 Low
Exploitation
<1% chance of attack within 30 days
Type
CWE-248Uncaught Exception
CWE-755Improper Handling of Exceptional Conditions
Timeline
Published16 Oct 2026
Updated16 Oct 2026
First seen16 Jul 2026
Sources
CVE-2026-62994 · MITRE
Track software like this
Free during beta