Skip to content

GHSA-74w3-63xv-x9mv CVE-2026-62994 GO-2026-6805

CoreDNS `k8s_external` headless AXFR can emit an empty transfer batch that panics the `transfer` plugin

Low Go
Severity
3.7 / 10
Low · CVSS 3.1
Exploit chance (EPSS)
0.46%
Chance of exploitation in the next 30 days. Higher than 39.83% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-248 Uncaught Exception
CWE-755 Improper Handling of Exceptional Conditions
Fix
1.14.5
Upgrade github.com/coredns/coredns to this version or later

Description

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
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:

  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:

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 versions

Go

PackageVulnerableFixed in
github.com/coredns/coredns
>= 1.9.4, < 1.14.5
1.14.5

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

github.com/coredns/coredns Go → 1.14.5

go
go get github.com/coredns/coredns@v1.14.5 && go mod tidy

Severity breakdown

CVSS 3.1 3.7

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L

Attack vector
Network
Attack complexity
High
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
Low

Weakness

CWE-248: Uncaught Exception

An exception is thrown from a function, but it is not caught.

CWE-755: Improper Handling of Exceptional Conditions

The product does not handle or incorrectly handles an exceptional condition.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-9gm5-9rfh-m6vx
CoreDNS DoH/DoQ/gRPC bypass UPDATE rejection enforced on UDP/TCP
High Also affects github.com/coredns/coredns
GHSA-mrg3-qvqr-jw29
CoreDNS: Unauthenticated memory exhaustion in custom transports
High Also affects github.com/coredns/coredns
GHSA-vp29-5652-4fw9
CoreDNS has TSIG authentication bypass on gRPC and QUIC transports
Critical Also affects github.com/coredns/coredns
GHSA-qhmp-q7xh-99rh
CoreDNS has TSIG authentication bypass on DoT, DoH, DoH3, DoQ, and gRPC
High Also affects github.com/coredns/coredns
GHSA-h8mm-c463-wjq3
CoreDNS' transfer stanza selection uses lexicographic compare (subzone ACL bypass)
High Also affects github.com/coredns/coredns
GHSA-63cw-r7xf-jmwr
CoreDNS DoH GET oversized dns= query parameter causes pre-validation CPU and memory amplification
High Also affects github.com/coredns/coredns
GHSA-2wpx-qpw2-g5h5
CoreDNS' DoQ worker pool does not bound stream backlog
High Also affects github.com/coredns/coredns
GHSA-h75p-j8xm-m278
CoreDNS Loop Detection Denial of Service Vulnerability
High Also affects github.com/coredns/coredns
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor