{"id":"CVE-2026-62994","kind":"vulnerability","aliases":["CVE-2026-62994","GHSA-74w3-63xv-x9mv","GO-2026-6805"],"related":[],"title":"CoreDNS `k8s_external` headless AXFR can emit an empty transfer batch that panics the `transfer` plugin","description":"### Summary\n\nA 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.\n\n### Details\n\n`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:\n\n- `plugin/transfer/transfer.go:129-131`\n\n```go\nfor records := range pchan {\n    if x, ok := records[0].(*dns.SOA); ok && soa == nil {\n        soa = x\n    }\n```\n\nThe 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:\n\n- `plugin/k8s_external/transfer.go:104-106`\n\n```go\nrecs, _ := e.srv(ctx, svcs, s)\nch <- recs\n```\n\n`plugin/k8s_external/msg_to_dns.go` intentionally skips services whose port is `-1`:\n\n- `plugin/k8s_external/msg_to_dns.go:107-112`\n\n```go\nfor _, s := range services {\n    // Don't add the entry if the port is -1 (invalid). The kubernetes plugin uses port -1 when a service/endpoint\n    // does not have any declared ports.\n    if s.Port == -1 {\n        continue\n    }\n```\n\nThe Kubernetes EndpointSlice conversion creates that sentinel when an EndpointSlice has no ports:\n\n- `plugin/kubernetes/object/endpoint.go:62-66`\n\n```go\nif len(ends.Ports) == 0 {\n    // Add sentinel if there are no ports.\n    e.Subsets[0].Ports = []EndpointPort{{Port: -1}}\n```\n\nThis creates a built-in source-to-sink chain:\n\n1. Kubernetes EndpointSlice has ready addresses and no ports.\n2. CoreDNS `kubernetes` converts it to an endpoint with `Port: -1`.\n3. `k8s_external` with `headless` includes that service in transfer data.\n4. `k8s_external.Transfer` sends an empty SRV `[]dns.RR` batch.\n5. `transfer.ServeDNS` indexes `records[0]` and panics.\n\nSuggested remediation:\n\n1. In `plugin/transfer/transfer.go`, skip or reject empty `records` batches before indexing `records[0]`.\n2. In `plugin/k8s_external/transfer.go`, only send `recs` when `len(recs) > 0` in the headless endpoint and port/protocol paths.\n3. Add regression tests covering `Port: -1` headless services and empty transfer batches.\n\nExample consumer-side guard:\n\n```go\nfor records := range pchan {\n    if len(records) == 0 {\n        continue\n    }\n    if x, ok := records[0].(*dns.SOA); ok && soa == nil {\n        soa = x\n    }\n    ...\n}\n```\n\n### PoC\n\nSafe local unit-level reproduction was performed. It does not contact external infrastructure and does not require a live Kubernetes cluster.\n\n#### Reproducer A: the `transfer` sink panics on an empty batch\n\nA temporary package test was added under `plugin/transfer`:\n\n```go\npackage transfer\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"testing\"\n\n    \"github.com/coredns/coredns/plugin/pkg/dnstest\"\n    \"github.com/coredns/coredns/plugin/test\"\n\n    \"github.com/miekg/dns\"\n)\n\ntype auditEmptyBatchTransferer struct{}\n\nfunc (auditEmptyBatchTransferer) Transfer(zone string, serial uint32) (<-chan []dns.RR, error) {\n    if zone != \"example.org.\" {\n        return nil, ErrNotAuthoritative\n    }\n    ch := make(chan []dns.RR, 3)\n    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))}\n    ch <- []dns.RR{}\n    close(ch)\n    return ch, nil\n}\n\nfunc TestAuditEmptyTransferBatchPanics(t *testing.T) {\n    tr := &Transfer{\n        Transferers: []Transferer{auditEmptyBatchTransferer{}},\n        xfrs:        []*xfr{{Zones: []string{\"example.org.\"}, to: []string{\"*\"}}},\n    }\n\n    req := new(dns.Msg)\n    req.SetQuestion(\"example.org.\", dns.TypeAXFR)\n    rec := dnstest.NewRecorder(&test.ResponseWriter{TCP: true})\n\n    defer func() {\n        if rec := recover(); rec == nil {\n            t.Fatal(\"expected empty transfer batch to panic\")\n        }\n    }()\n\n    _, _ = tr.ServeDNS(context.Background(), rec, req)\n}\n```\n\nVerification command and result:\n\n```text\n$ go test ./plugin/transfer -run TestAuditEmptyTransferBatchPanics -count=1 -v\n=== RUN   TestAuditEmptyTransferBatchPanics\n--- PASS: TestAuditEmptyTransferBatchPanics (0.00s)\nPASS\nok  \tgithub.com/coredns/coredns/plugin/transfer\t0.005s\n```\n\n#### Reproducer B: the built-in `k8s_external` transferer emits the empty batch\n\nA temporary package test was added under `plugin/k8s_external`:\n\n```go\npackage external\n\nimport (\n    \"testing\"\n    \"time\"\n\n    \"github.com/coredns/coredns/plugin/etcd/msg\"\n    \"github.com/coredns/coredns/plugin/kubernetes\"\n    \"github.com/coredns/coredns/request\"\n\n    \"github.com/miekg/dns\"\n)\n\nfunc TestAuditHeadlessNoPortTransferEmitsEmptyBatch(t *testing.T) {\n    e := New()\n    e.Zones = []string{\"example.com.\"}\n    e.headless = true\n    e.externalAddrFunc = func(request.Request, bool) []dns.RR { return nil }\n    e.externalSerialFunc = func(string) uint32 { return 1 }\n    e.externalServicesFunc = func(string, bool) ([]msg.Service, map[string][]msg.Service) {\n        return nil, map[string][]msg.Service{\n            \"/coredns/com/example/testns/headless/\" + kubernetes.Endpoint: {{Host: \"192.0.2.10\", Port: -1, TTL: 5, Key: \"/coredns/com/example/testns/headless/endpoint-a\"}},\n        }\n    }\n\n    ch, err := e.Transfer(\"example.com.\", 0)\n    if err != nil {\n        t.Fatalf(\"Transfer returned error: %v\", err)\n    }\n\n    deadline := time.After(time.Second)\n    for {\n        select {\n        case batch, ok := <-ch:\n            if !ok {\n                t.Fatal(\"transfer closed before emitting an empty batch\")\n            }\n            if len(batch) == 0 {\n                return\n            }\n        case <-deadline:\n            t.Fatal(\"timed out waiting for an empty batch\")\n        }\n    }\n}\n```\n\nVerification command and result:\n\n```text\n$ go test ./plugin/k8s_external -run TestAuditHeadlessNoPortTransferEmitsEmptyBatch -count=1 -v\n=== RUN   TestAuditHeadlessNoPortTransferEmitsEmptyBatch\n--- PASS: TestAuditHeadlessNoPortTransferEmitsEmptyBatch (0.00s)\nPASS\nok  \tgithub.com/coredns/coredns/plugin/k8s_external\t0.012s\n```\n\nAdditional focused verification:\n\n```text\n$ go test ./plugin/transfer ./plugin/k8s_external ./plugin/kubernetes -run 'TestAudit|^$' -count=1 -v\nok  \tgithub.com/coredns/coredns/plugin/transfer\t0.006s\nok  \tgithub.com/coredns/coredns/plugin/k8s_external\t0.013s\nok  \tgithub.com/coredns/coredns/plugin/kubernetes\t0.116s\n```\n\nAffected package suites:\n\n```text\n$ go test ./plugin/transfer ./plugin/k8s_external ./plugin/kubernetes -count=1\nok  \tgithub.com/coredns/coredns/plugin/transfer\t0.010s\nok  \tgithub.com/coredns/coredns/plugin/k8s_external\t0.011s\nok  \tgithub.com/coredns/coredns/plugin/kubernetes\t0.194s\n```\n\n### Impact\n\nA network client allowed to request AXFR for the affected zone can trigger a panic in CoreDNS when all prerequisites are present.\n\nRequired configuration/state:\n\n```corefile\nexample.com:53 {\n    kubernetes cluster.local\n    k8s_external example.com {\n        headless\n    }\n    transfer example.com {\n        to *\n    }\n}\n```\n\nAdditional prerequisite:\n\n- 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}`.\n\nIn 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.","summary":"CoreDNS panics on an AXFR request for a zone served by k8s_external with headless enabled when a headless service endpoint has no declared ports, because an empty record batch is indexed without a length check. A network client allowed to request zone transfers can trigger repeated panics, returning SERVFAIL in normal mode and crashing the process if the debug plugin disables panic recovery.","severity":"low","cvss":3.700000047683716,"epss":0.0045900000259280205,"exploited":false,"exploited_at":null,"products":[{"name":"github.com/coredns/coredns","fixed":["1.14.5"],"affected":">= 1.9.4, <= 1.14.4","ecosystem":"go"},{"name":"github.com/coredns/coredns","fixed":["1.14.5"],"affected":">= 1.9.4, < 1.14.5","ecosystem":"go"},{"name":"coredns.io coredns","fixed":["1.14.5"],"affected":">= 1.9.4, < 1.14.5","ecosystem":"vendor"}],"ecosystems":["go","vendor"],"tags":["coredns","cwe-248","cwe-755","go","kubernetes","package","self-hosted"],"not_affected_if":["managed-hosting","no-headless-k8s-external","no-transfer-to-untrusted-clients","version-1.14.5-or-later"],"how_to_check":"Check whether the Corefile has both a k8s_external block with headless and a transfer block with a to directive that allows clients, e.g. grep -nE 'k8s_external|headless|transfer' Corefile.\nCheck the CoreDNS version with coredns -version or the go.mod entry for github.com/coredns/coredns.\nCheck whether the debug plugin is enabled in the Corefile.","fix":"Upgrade github.com/coredns/coredns to 1.14.5 or later.\nUntil upgraded, restrict transfer to specific IPs instead of '*' or remove the headless option from k8s_external.","status":"open","published_at":"2026-07-16T20:16:47+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","triaged_at":"2026-10-23T20:03:32+00:00","updated_at":"2026-10-23T23:44:52+00:00","category":"package","confidence":0.85,"triage":{"at":"2026-10-23T20:03:32Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade github.com/coredns/coredns to 1.14.5 or later.\nUntil upgraded, restrict transfer to specific IPs instead of '*' or remove the headless option from k8s_external.","summary":"CoreDNS panics on an AXFR request for a zone served by k8s_external with headless enabled when a headless service endpoint has no declared ports, because an empty record batch is indexed without a length check. A network client allowed to request zone transfers can trigger repeated panics, returning SERVFAIL in normal mode and crashing the process if the debug plugin disables panic recovery.","category":"package","who_cares":["coredns","kubernetes","go","self-hosted"],"confidence":0.85,"analyst_note":"","how_to_check":"Check whether the Corefile has both a k8s_external block with headless and a transfer block with a to directive that allows clients, e.g. grep -nE 'k8s_external|headless|transfer' Corefile.\nCheck the CoreDNS version with coredns -version or the go.mod entry for github.com/coredns/coredns.\nCheck whether the debug plugin is enabled in the Corefile.","needs_analyst":false,"not_affected_if":["version-1.14.5-or-later","no-headless-k8s-external","no-transfer-to-untrusted-clients","managed-hosting"],"severity_if_unknown":"low"},"cost_usd":0.000788,"input_tokens":5528,"output_tokens":470,"severity_before":"low"},"analyst_status":null,"analyst_note":null,"search":"'-1':309C,327C,334C,346C,381C,412C,477C '-106':293C '-112':312C '-131':248C '-2026':18A,26A,29A '-248':41B '-62994':19A,30A '-66':363C '-6805':27A '-755':43B '/coredns/coredns':33A,36A '/coredns/coredns/plugin/pkg/dnstest':560C '/coredns/coredns/plugin/test':563C '/miekg/dns':566C '0':174C,240C,258C,368C,377C,436C,453C,463C,497C,503C '1':393C,441C,611C '100':596C,607C '104':292C '107':311C '129':247C '1800':605C '2':402C,454C '3':413C,471C,590C '4':424C '5':432C '62':362C '63xv':23A '7200':604C '74w3':22A '86400':606C 'add':320C,369C,472C,548C 'address':398C 'allow':89B,119C 'amplifi':203C 'assum':222C 'auditemptybatchtransfer':568C,571C,627C 'axfr':5A,54B,122C 'axfr/ixfr':234C 'batch':11A,79B,166C,179C,232C,431C,449C,483C,542C 'built':156C,267C,386C 'built-in':155C,266C,385C 'ch':300C,586C,591C,612C,615C,617C 'chain':392C 'chan':577C,588C 'check':85B,176C,242C 'client':88B,118C 'close':614C 'cluster':532C 'configur':134C 'consum':486C 'consumer-sid':485C 'contact':522C 'contain':145C 'context':555C 'continu':347C,498C 'contract':274C 'convers':351C 'convert':405C 'coredn':1A,39B,50B,125C,132C,186C,218C,403C 'coredns':38A 'coredns.io':37A 'cover':475C 'crash':104B,216C 'creat':352C,383C 'ctx':297C 'cve':17A,28A 'cwe':40B,42B 'd':603C 'data':423C 'debug':109B,208C 'declar':73B,152C,282C,342C 'detail':220C 'disabl':111B,210C 'disrupt':200C 'dns':117C 'dns.rr':165C,231C,430C,578C,589C,592C,613C 'dns.soa':259C,504C 'e.srv':296C 'e.subsets':376C 'emit':7A,162C,226C 'empti':9A,77B,164C,183C,230C,428C,447C,481C,541C 'enabl':65B 'endpoint':70B,149C,279C,409C,467C 'endpointport':379C 'endpointslic':350C,357C,395C 'ends.ports':367C 'entri':322C 'errnotauthorit':585C 'error':579C 'everi':224C 'exampl':484C 'example.org':582C 'extern':3A,62B,137C,159C,270C,415C,523C 'fmt':556C 'fmt.sprintf':594C 'func':570C,619C 'generat':236C 'ghsa':21A 'ghsa-74w3-63xv-x9mv':20A 'github.com':32A,35A,559C,562C,565C 'github.com/coredns/coredns':31A,34A 'github.com/coredns/coredns/plugin/pkg/dnstest':558C 'github.com/coredns/coredns/plugin/test':561C 'github.com/miekg/dns':564C 'go':25A,44B,249C,294C,313C,364C,489C,551C 'guard':488C 'headless':4A,64B,68B,139C,147C,276C,417C,466C,478C 'headless-servic':138C 'host':49B 'hostmast':601C 'import':554C 'includ':418C 'index':81B,172C,238C,434C,451C 'infrastructur':524C 'intent':303C 'invalid':328C 'k8s':2A,61B,136C,158C,269C,414C 'k8s_external.transfer':425C 'kubernet':45B,144C,330C,349C,394C,404C,531C 'len':366C,461C,495C 'length':84B,245C 'level':515C 'live':530C 'local':512C 'log':205C 'make':587C 'mode':102B,187C 'network':87B,116C 'nil':262C,507C,584C,618C 'non':182C,229C 'non-empti':181C,228C 'normal':101B,185C 'ns.dns':599C 'ok':256C,260C,501C,505C 'packag':46B,545C,552C 'panic':13A,51B,97B,112B,130C,189C,204C,438C,538C 'path':214C,470C 'pchan':253C,493C 'perform':518C 'plugin':16A,110B,171C,209C,331C 'plugin/k8s_external/msg_to_dns.go':302C,310C 'plugin/k8s_external/transfer.go':284C,291C,456C 'plugin/kubernetes/object/endpoint.go':361C 'plugin/transfer':550C 'plugin/transfer/transfer.go':221C,246C,443C 'poc':510C 'port':74B,153C,283C,307C,325C,333C,343C,360C,375C,378C,380C,401C,411C,476C 'port/protocol':469C 'process':106B,219C 'rang':252C,316C,492C 'readi':397C 'rec':295C,301C,459C,462C 'record':78B,173C,239C,251C,257C,435C,448C,452C,491C,496C,502C 'recov':191C 'recoveri':113B,211C 'regress':473C 'reject':446C 'remedi':440C 'repeat':96B,197C 'reproduc':533C 'reproduct':516C 'request':55B,91B,121C,201C 'requir':528C 'respons':235C 'result':288C 'return':98B,193C,583C,616C 's.port':345C 'safe':511C 'self':48B 'self-host':47B 'send':285C,426C,458C 'sentinel':354C,370C 'serial':575C 'serv':59B 'servfail':99B,195C 'servic':69B,140C,148C,277C,305C,317C,420C,479C 'service/endpoint':337C 'share':169C 'side':487C 'sink':391C,537C 'skip':304C,444C 'slice':244C,289C 'soa':261C,263C,506C,508C,598C 'sourc':389C 'source-to-sink':388C 'srv':287C,429C 'string':574C 'struct':569C 'suggest':439C 'summari':114C 'svcs':298C 'temporari':544C 'test':474C,546C,557C 'test.soa':593C 'testauditemptytransferbatchpan':620C 'testing.t':622C 'tr':623C 'transfer':10A,15A,93B,142C,160C,170C,225C,271C,422C,482C,536C,553C,572C,624C,625C,626C 'transfer.servedns':433C 'trigger':95B,128C,198C 'type':567C 'uint32':576C 'uncondit':290C 'unit':514C 'unit-level':513C 'use':332C 'violat':272C 'whose':278C,306C 'without':82B,175C,241C 'x':255C,264C,500C,509C 'x9mv':24A 'xfr':629C 'xfrs':628C 'zone':58B,92B,126C,141C,573C,581C,608C,609C,610C","nicknames":[],"mitigations":[],"who_affected":null,"tier":null,"verified":{},"analyst_revisit_at":null,"first_public_at":"2026-07-16T20:16:47+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-74w3-63xv-x9mv","url":"https://github.com/advisories/GHSA-74w3-63xv-x9mv","severity":"low","published_at":"2026-10-16T19:26:42+00:00","modified_at":"2026-10-16T19:26:43+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","fetched_at":"2026-10-23T19:11:32+00:00","note":null},{"source":"govuln","external_id":"GO-2026-6805","url":"https://pkg.go.dev/vuln/GO-2026-6805","severity":null,"published_at":"2026-10-19T21:08:35+00:00","modified_at":"2026-10-19T21:08:35+00:00","first_seen_at":"2026-10-23T19:13:08+00:00","fetched_at":"2026-10-23T19:13:08+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-62994","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-62994","severity":null,"published_at":"2026-07-16T20:16:47+00:00","modified_at":"2026-07-22T20:14:36+00:00","first_seen_at":"2026-10-23T23:44:52+00:00","fetched_at":"2026-10-23T23:44:52+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}