summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRoss Burton <ross.burton@arm.com>2025-11-03 14:21:46 +0000
committerRichard Purdie <richard.purdie@linuxfoundation.org>2025-11-06 15:09:32 +0000
commit1db7c5487bb9c20b40efef7c31d2a0ec31620d0d (patch)
tree63a777f7f1107cb5ec06da015e65783962048e68
parent310183b813dea5898aff8d425b5d5c5063af354a (diff)
downloadpoky-1db7c5487bb9c20b40efef7c31d2a0ec31620d0d.tar.gz
kea: fix CVE-2025-11232
Backport a patch from upstream to resolve CVE-2025-11232: Invalid characters cause assert To trigger the issue, three configuration parameters must have specific settings: "hostname-char-set" must be left at the default setting, which is "[^A-Za-z0-9.-]"; "hostname-char-replacement" must be empty (the default); and "ddns-qualifying-suffix" must NOT be empty (the default is empty). DDNS updates do not need to be enabled for this issue to manifest. A client that sends certain option content would then cause kea-dhcp4 to exit unexpectedly. (From OE-Core rev: f9331b42fd8b0df64517969a794a93d41624bd96) Signed-off-by: Ross Burton <ross.burton@arm.com> Signed-off-by: Mathieu Dubois-Briand <mathieu.dubois-briand@bootlin.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
-rw-r--r--meta/recipes-connectivity/kea/files/CVE-2025-11232.patch474
-rw-r--r--meta/recipes-connectivity/kea/kea_3.0.1.bb1
2 files changed, 475 insertions, 0 deletions
diff --git a/meta/recipes-connectivity/kea/files/CVE-2025-11232.patch b/meta/recipes-connectivity/kea/files/CVE-2025-11232.patch
new file mode 100644
index 0000000000..659627deba
--- /dev/null
+++ b/meta/recipes-connectivity/kea/files/CVE-2025-11232.patch
@@ -0,0 +1,474 @@
1From 92b65b2345e07d826b56ffd65cf47538f1c7a271 Mon Sep 17 00:00:00 2001
2From: Thomas Markwalder <tmark@isc.org>
3Date: Tue, 7 Oct 2025 14:41:16 -0400
4Subject: [PATCH] [#4155] Backport #4142 to v3_0
5
6Invalid characters cause assert
7
8To trigger the issue, three configuration parameters must have
9specific settings: "hostname-char-set" must be left at the default
10setting, which is "[^A-Za-z0-9.-]"; "hostname-char-replacement" must
11be empty (the default); and "ddns-qualifying-suffix" must NOT be empty
12(the default is empty). DDNS updates do not need to be enabled for
13this issue to manifest. A client that sends certain option content
14would then cause kea-dhcp4 to exit unexpectedly.
15
16CVE: CVE-2025-11232
17Upstream-Status: Backport [https://github.com/isc-projects/kea/commit/92b65b2345e07d826b56ffd65cf47538f1c7a271]
18Signed-off-by: Ross Burton <ross.burton@arm.com>
19
20new file: changelog_unreleased/CVE-2025-11232-catch-empty-sanitized-hostname
21modified: src/bin/dhcp4/dhcp4_messages.cc
22modified: src/bin/dhcp4/dhcp4_messages.h
23modified: src/bin/dhcp4/dhcp4_messages.mes
24modified: src/bin/dhcp4/dhcp4_srv.cc
25modified: src/bin/dhcp4/tests/fqdn_unittest.cc
26modified: src/bin/dhcp6/dhcp6_messages.cc
27modified: src/bin/dhcp6/dhcp6_messages.h
28modified: src/bin/dhcp6/dhcp6_messages.mes
29modified: src/bin/dhcp6/dhcp6_srv.cc
30modified: src/bin/dhcp6/tests/fqdn_unittest.cc
31modified: src/lib/dhcpsrv/d2_client_mgr.cc
32modified: src/lib/dhcpsrv/d2_client_mgr.h
33modified: src/lib/dhcpsrv/tests/d2_client_unittest.cc
34---
35 ...-2025-11232-catch-empty-sanitized-hostname | 6 +++
36 src/bin/dhcp4/dhcp4_messages.cc | 4 ++
37 src/bin/dhcp4/dhcp4_messages.h | 2 +
38 src/bin/dhcp4/dhcp4_messages.mes | 14 +++++
39 src/bin/dhcp4/dhcp4_srv.cc | 21 ++++++--
40 src/bin/dhcp4/tests/fqdn_unittest.cc | 54 ++++++++++++++++++-
41 src/bin/dhcp6/dhcp6_messages.cc | 2 +
42 src/bin/dhcp6/dhcp6_messages.h | 1 +
43 src/bin/dhcp6/dhcp6_messages.mes | 7 +++
44 src/bin/dhcp6/dhcp6_srv.cc | 9 +++-
45 src/bin/dhcp6/tests/fqdn_unittest.cc | 23 ++++++++
46 src/lib/dhcpsrv/d2_client_mgr.cc | 9 +++-
47 src/lib/dhcpsrv/d2_client_mgr.h | 19 ++++++-
48 src/lib/dhcpsrv/tests/d2_client_unittest.cc | 42 +++++++++++++++
49 14 files changed, 205 insertions(+), 8 deletions(-)
50 create mode 100644 changelog_unreleased/CVE-2025-11232-catch-empty-sanitized-hostname
51
52diff --git a/src/bin/dhcp4/dhcp4_messages.cc b/src/bin/dhcp4/dhcp4_messages.cc
53index e06ce6a121..5c6a334bad 100644
54--- a/src/bin/dhcp4/dhcp4_messages.cc
55+++ b/src/bin/dhcp4/dhcp4_messages.cc
56@@ -26,9 +26,11 @@ extern const isc::log::MessageID DHCP4_CLASS_UNCONFIGURED = "DHCP4_CLASS_UNCONFI
57 extern const isc::log::MessageID DHCP4_CLIENTID_IGNORED_FOR_LEASES = "DHCP4_CLIENTID_IGNORED_FOR_LEASES";
58 extern const isc::log::MessageID DHCP4_CLIENT_FQDN_DATA = "DHCP4_CLIENT_FQDN_DATA";
59 extern const isc::log::MessageID DHCP4_CLIENT_FQDN_PROCESS = "DHCP4_CLIENT_FQDN_PROCESS";
60+extern const isc::log::MessageID DHCP4_CLIENT_FQDN_SCRUBBED_EMPTY = "DHCP4_CLIENT_FQDN_SCRUBBED_EMPTY";
61 extern const isc::log::MessageID DHCP4_CLIENT_HOSTNAME_DATA = "DHCP4_CLIENT_HOSTNAME_DATA";
62 extern const isc::log::MessageID DHCP4_CLIENT_HOSTNAME_MALFORMED = "DHCP4_CLIENT_HOSTNAME_MALFORMED";
63 extern const isc::log::MessageID DHCP4_CLIENT_HOSTNAME_PROCESS = "DHCP4_CLIENT_HOSTNAME_PROCESS";
64+extern const isc::log::MessageID DHCP4_CLIENT_HOSTNAME_SCRUBBED_EMPTY = "DHCP4_CLIENT_HOSTNAME_SCRUBBED_EMPTY";
65 extern const isc::log::MessageID DHCP4_CLIENT_NAME_PROC_FAIL = "DHCP4_CLIENT_NAME_PROC_FAIL";
66 extern const isc::log::MessageID DHCP4_CONFIG_COMPLETE = "DHCP4_CONFIG_COMPLETE";
67 extern const isc::log::MessageID DHCP4_CONFIG_LOAD_FAIL = "DHCP4_CONFIG_LOAD_FAIL";
68@@ -206,9 +208,11 @@ const char* values[] = {
69 "DHCP4_CLIENTID_IGNORED_FOR_LEASES", "%1: not using client identifier for lease allocation for subnet %2",
70 "DHCP4_CLIENT_FQDN_DATA", "%1: Client sent FQDN option: %2",
71 "DHCP4_CLIENT_FQDN_PROCESS", "%1: processing Client FQDN option",
72+ "DHCP4_CLIENT_FQDN_SCRUBBED_EMPTY", "%1: sanitizing client's FQDN option '%2' yielded an empty string",
73 "DHCP4_CLIENT_HOSTNAME_DATA", "%1: client sent Hostname option: %2",
74 "DHCP4_CLIENT_HOSTNAME_MALFORMED", "%1: client hostname option malformed: %2",
75 "DHCP4_CLIENT_HOSTNAME_PROCESS", "%1: processing client's Hostname option",
76+ "DHCP4_CLIENT_HOSTNAME_SCRUBBED_EMPTY", "%1: sanitizing client's Hostname option '%2' yielded an empty string",
77 "DHCP4_CLIENT_NAME_PROC_FAIL", "%1: failed to process the fqdn or hostname sent by a client: %2",
78 "DHCP4_CONFIG_COMPLETE", "DHCPv4 server has completed configuration: %1",
79 "DHCP4_CONFIG_LOAD_FAIL", "configuration error using file: %1, reason: %2",
80diff --git a/src/bin/dhcp4/dhcp4_messages.h b/src/bin/dhcp4/dhcp4_messages.h
81index 9a4d0cda21..6e45c63053 100644
82--- a/src/bin/dhcp4/dhcp4_messages.h
83+++ b/src/bin/dhcp4/dhcp4_messages.h
84@@ -27,9 +27,11 @@ extern const isc::log::MessageID DHCP4_CLASS_UNCONFIGURED;
85 extern const isc::log::MessageID DHCP4_CLIENTID_IGNORED_FOR_LEASES;
86 extern const isc::log::MessageID DHCP4_CLIENT_FQDN_DATA;
87 extern const isc::log::MessageID DHCP4_CLIENT_FQDN_PROCESS;
88+extern const isc::log::MessageID DHCP4_CLIENT_FQDN_SCRUBBED_EMPTY;
89 extern const isc::log::MessageID DHCP4_CLIENT_HOSTNAME_DATA;
90 extern const isc::log::MessageID DHCP4_CLIENT_HOSTNAME_MALFORMED;
91 extern const isc::log::MessageID DHCP4_CLIENT_HOSTNAME_PROCESS;
92+extern const isc::log::MessageID DHCP4_CLIENT_HOSTNAME_SCRUBBED_EMPTY;
93 extern const isc::log::MessageID DHCP4_CLIENT_NAME_PROC_FAIL;
94 extern const isc::log::MessageID DHCP4_CONFIG_COMPLETE;
95 extern const isc::log::MessageID DHCP4_CONFIG_LOAD_FAIL;
96diff --git a/src/bin/dhcp4/dhcp4_messages.mes b/src/bin/dhcp4/dhcp4_messages.mes
97index 1deb2e6074..b359d09616 100644
98--- a/src/bin/dhcp4/dhcp4_messages.mes
99+++ b/src/bin/dhcp4/dhcp4_messages.mes
100@@ -164,6 +164,20 @@ This debug message is issued when the server starts processing the Hostname
101 option sent in the client's query. The argument includes the client and
102 transaction identification information.
103
104+% DHCP4_CLIENT_HOSTNAME_SCRUBBED_EMPTY %1: sanitizing client's Hostname option '%2' yielded an empty string
105+Logged at debug log level 50.
106+This debug message is issued when the result of sanitizing the
107+hostname option(12) sent by the client is an empty string. When this occurs
108+the server will ignore the hostname option. The arguments include the
109+client and the hostname option it sent.
110+
111+% DHCP4_CLIENT_FQDN_SCRUBBED_EMPTY %1: sanitizing client's FQDN option '%2' yielded an empty string
112+Logged at debug log level 50.
113+This debug message is issued when the result of sanitizing the
114+FQDN option(81) sent by the client is an empty string. When this occurs
115+the server will ignore the FQDN option. The arguments include the
116+client and the FQDN option it sent.
117+
118 % DHCP4_CLIENT_NAME_PROC_FAIL %1: failed to process the fqdn or hostname sent by a client: %2
119 Logged at debug log level 55.
120 This debug message is issued when the DHCP server was unable to process the
121diff --git a/src/bin/dhcp4/dhcp4_srv.cc b/src/bin/dhcp4/dhcp4_srv.cc
122index 0701ed41e9..a6be662889 100644
123--- a/src/bin/dhcp4/dhcp4_srv.cc
124+++ b/src/bin/dhcp4/dhcp4_srv.cc
125@@ -2714,8 +2714,15 @@ Dhcpv4Srv::processClientFqdnOption(Dhcpv4Exchange& ex) {
126 } else {
127 // Adjust the domain name based on domain name value and type sent by the
128 // client and current configuration.
129- d2_mgr.adjustDomainName<Option4ClientFqdn>(*fqdn, *fqdn_resp,
130- *(ex.getContext()->getDdnsParams()));
131+ try {
132+ d2_mgr.adjustDomainName<Option4ClientFqdn>(*fqdn, *fqdn_resp,
133+ *(ex.getContext()->getDdnsParams()));
134+ } catch (const FQDNScrubbedEmpty& scrubbed) {
135+ LOG_DEBUG(ddns4_logger, DBG_DHCP4_DETAIL, DHCP4_CLIENT_FQDN_SCRUBBED_EMPTY)
136+ .arg(ex.getQuery()->getLabel())
137+ .arg(scrubbed.what());
138+ return;
139+ }
140 }
141
142 // Add FQDN option to the response message. Note that, there may be some
143@@ -2857,7 +2864,15 @@ Dhcpv4Srv::processHostnameOption(Dhcpv4Exchange& ex) {
144 ex.getContext()->getDdnsParams()->getHostnameSanitizer();
145
146 if (sanitizer) {
147- hostname = sanitizer->scrub(hostname);
148+ auto tmp = sanitizer->scrub(hostname);
149+ if (tmp.empty()) {
150+ LOG_DEBUG(ddns4_logger, DBG_DHCP4_DETAIL, DHCP4_CLIENT_HOSTNAME_SCRUBBED_EMPTY)
151+ .arg(ex.getQuery()->getLabel())
152+ .arg(hostname);
153+ return;
154+ }
155+
156+ hostname = tmp;
157 }
158
159 // Convert hostname to lower case.
160diff --git a/src/bin/dhcp4/tests/fqdn_unittest.cc b/src/bin/dhcp4/tests/fqdn_unittest.cc
161index a5d3e4c21a..18e4c6d4b9 100644
162--- a/src/bin/dhcp4/tests/fqdn_unittest.cc
163+++ b/src/bin/dhcp4/tests/fqdn_unittest.cc
164@@ -2253,7 +2253,7 @@ TEST_F(NameDhcpv4SrvTest, sanitizeHostDefault) {
165 },
166 {
167 "qualified host name with nuls",
168- std::string("four-ok-host\000.other.org",23),
169+ std::string("four-ok-host\000.other.org", 23),
170 "four-ok-host.other.org"
171 }
172 };
173@@ -3203,4 +3203,56 @@ TEST_F(NameDhcpv4SrvTest, poolDdnsParametersTest) {
174 }
175 }
176
177+// Verifies that when the FQDN option is scrubbed empty it is logged
178+// and ignored.
179+TEST_F(NameDhcpv4SrvTest, hostnameScrubbedEmpty) {
180+ Dhcp4Client client(srv_, Dhcp4Client::SELECTING);
181+
182+ // Configure DHCP server.
183+ configure(CONFIGS[2], *client.getServer());
184+
185+ // Set the hostname option.
186+ ASSERT_NO_THROW(client.includeHostname("___"));
187+
188+ // Send the DHCPDISCOVER and make sure that the server responded.
189+ ASSERT_NO_THROW(client.doDiscover());
190+ auto resp = client.getContext().response_;
191+ ASSERT_TRUE(resp);
192+ ASSERT_EQ(DHCPOFFER, static_cast<int>(resp->getType()));
193+
194+ // Should have logged that it was scrubbed empty.
195+ std::string log = "DHCP4_CLIENT_HOSTNAME_SCRUBBED_EMPTY";
196+ EXPECT_EQ(1, countFile(log));
197+
198+ // Hostname should not be in the response.
199+ ASSERT_FALSE(resp->getOption(DHO_HOST_NAME));
200+}
201+
202+// Verifies that when the FQDN option is scrubbed empty it is logged
203+// and ignored.
204+TEST_F(NameDhcpv4SrvTest, fqdnScrubbedEmpty) {
205+ Dhcp4Client client(srv_, Dhcp4Client::SELECTING);
206+
207+ // Configure DHCP server.
208+ configure(CONFIGS[2], *client.getServer());
209+
210+ // Include the Client FQDN option.
211+ ASSERT_NO_THROW(client.includeFQDN(Option4ClientFqdn::FLAG_S | Option4ClientFqdn::FLAG_E,
212+ "___", Option4ClientFqdn::PARTIAL));
213+
214+ // Send the DHCPDISCOVER and make sure that the server responded.
215+ ASSERT_NO_THROW(client.doDiscover());
216+ auto resp = client.getContext().response_;
217+ ASSERT_TRUE(resp);
218+ ASSERT_EQ(DHCPOFFER, static_cast<int>(resp->getType()));
219+
220+ // Should have logged that it was scrubbed empty.
221+ std::string log = "DHCP4_CLIENT_FQDN_SCRUBBED_EMPTY";
222+ EXPECT_EQ(1, countFile(log));
223+
224+ // Hostname should not be in the response.
225+ ASSERT_FALSE(resp->getOption(DHO_FQDN));
226+}
227+
228+
229 } // end of anonymous namespace
230diff --git a/src/bin/dhcp6/dhcp6_messages.cc b/src/bin/dhcp6/dhcp6_messages.cc
231index 229ba74450..9619481aba 100644
232--- a/src/bin/dhcp6/dhcp6_messages.cc
233+++ b/src/bin/dhcp6/dhcp6_messages.cc
234@@ -27,6 +27,7 @@ extern const isc::log::MessageID DHCP6_CLASSES_ASSIGNED = "DHCP6_CLASSES_ASSIGNE
235 extern const isc::log::MessageID DHCP6_CLASSES_ASSIGNED_AFTER_SUBNET_SELECTION = "DHCP6_CLASSES_ASSIGNED_AFTER_SUBNET_SELECTION";
236 extern const isc::log::MessageID DHCP6_CLASS_ASSIGNED = "DHCP6_CLASS_ASSIGNED";
237 extern const isc::log::MessageID DHCP6_CLASS_UNCONFIGURED = "DHCP6_CLASS_UNCONFIGURED";
238+extern const isc::log::MessageID DHCP6_CLIENT_FQDN_SCRUBBED_EMPTY = "DHCP6_CLIENT_FQDN_SCRUBBED_EMPTY";
239 extern const isc::log::MessageID DHCP6_CONFIG_COMPLETE = "DHCP6_CONFIG_COMPLETE";
240 extern const isc::log::MessageID DHCP6_CONFIG_LOAD_FAIL = "DHCP6_CONFIG_LOAD_FAIL";
241 extern const isc::log::MessageID DHCP6_CONFIG_PACKET_QUEUE = "DHCP6_CONFIG_PACKET_QUEUE";
242@@ -203,6 +204,7 @@ const char* values[] = {
243 "DHCP6_CLASSES_ASSIGNED_AFTER_SUBNET_SELECTION", "%1: client packet has been assigned to the following classes: %2",
244 "DHCP6_CLASS_ASSIGNED", "%1: client packet has been assigned to the following class: %2",
245 "DHCP6_CLASS_UNCONFIGURED", "%1: client packet belongs to an unconfigured class: %2",
246+ "DHCP6_CLIENT_FQDN_SCRUBBED_EMPTY", "%1: sanitizing client's FQDN option '%2' yielded an empty string",
247 "DHCP6_CONFIG_COMPLETE", "DHCPv6 server has completed configuration: %1",
248 "DHCP6_CONFIG_LOAD_FAIL", "configuration error using file: %1, reason: %2",
249 "DHCP6_CONFIG_PACKET_QUEUE", "DHCPv6 packet queue info after configuration: %1",
250diff --git a/src/bin/dhcp6/dhcp6_messages.h b/src/bin/dhcp6/dhcp6_messages.h
251index 186f7d557a..7af56e716a 100644
252--- a/src/bin/dhcp6/dhcp6_messages.h
253+++ b/src/bin/dhcp6/dhcp6_messages.h
254@@ -28,6 +28,7 @@ extern const isc::log::MessageID DHCP6_CLASSES_ASSIGNED;
255 extern const isc::log::MessageID DHCP6_CLASSES_ASSIGNED_AFTER_SUBNET_SELECTION;
256 extern const isc::log::MessageID DHCP6_CLASS_ASSIGNED;
257 extern const isc::log::MessageID DHCP6_CLASS_UNCONFIGURED;
258+extern const isc::log::MessageID DHCP6_CLIENT_FQDN_SCRUBBED_EMPTY;
259 extern const isc::log::MessageID DHCP6_CONFIG_COMPLETE;
260 extern const isc::log::MessageID DHCP6_CONFIG_LOAD_FAIL;
261 extern const isc::log::MessageID DHCP6_CONFIG_PACKET_QUEUE;
262diff --git a/src/bin/dhcp6/dhcp6_messages.mes b/src/bin/dhcp6/dhcp6_messages.mes
263index fff50ed367..79fc984ff5 100644
264--- a/src/bin/dhcp6/dhcp6_messages.mes
265+++ b/src/bin/dhcp6/dhcp6_messages.mes
266@@ -1167,3 +1167,10 @@ such modification. The clients will remember previous server-id, and will
267 use it to extend their leases. As a result, they will have to go through
268 a rebinding phase to re-acquire their leases and associate them with a
269 new server id.
270+
271+% DHCP6_CLIENT_FQDN_SCRUBBED_EMPTY %1: sanitizing client's FQDN option '%2' yielded an empty string
272+Logged at debug log level 50.
273+This debug message is issued when the result of sanitizing the
274+FQDN option(39) sent by the client is an empty string. When this occurs
275+the server will ignore the FQDN option. The arguments include the
276+client and the FQDN option it sent.
277diff --git a/src/bin/dhcp6/dhcp6_srv.cc b/src/bin/dhcp6/dhcp6_srv.cc
278index 417960b126..f999c3178f 100644
279--- a/src/bin/dhcp6/dhcp6_srv.cc
280+++ b/src/bin/dhcp6/dhcp6_srv.cc
281@@ -2332,7 +2332,14 @@ Dhcpv6Srv::processClientFqdn(const Pkt6Ptr& question, const Pkt6Ptr& answer,
282 } else {
283 // Adjust the domain name based on domain name value and type sent by
284 // the client and current configuration.
285- d2_mgr.adjustDomainName<Option6ClientFqdn>(*fqdn, *fqdn_resp, *ddns_params);
286+ try {
287+ d2_mgr.adjustDomainName<Option6ClientFqdn>(*fqdn, *fqdn_resp, *ddns_params);
288+ } catch(const FQDNScrubbedEmpty& scrubbed) {
289+ LOG_DEBUG(ddns6_logger, DBG_DHCP6_DETAIL, DHCP6_CLIENT_FQDN_SCRUBBED_EMPTY)
290+ .arg(question->getLabel())
291+ .arg(scrubbed.what());
292+ return;
293+ }
294 }
295
296 // Once we have the FQDN setup to use it for the lease hostname. This
297diff --git a/src/bin/dhcp6/tests/fqdn_unittest.cc b/src/bin/dhcp6/tests/fqdn_unittest.cc
298index ca51856e67..7891c1f5e6 100644
299--- a/src/bin/dhcp6/tests/fqdn_unittest.cc
300+++ b/src/bin/dhcp6/tests/fqdn_unittest.cc
301@@ -2425,4 +2425,27 @@ TEST_F(FqdnDhcpv6SrvTest, poolDdnsParametersTest) {
302 }
303 }
304
305+// Verify an FQDN with all invalid chars is ignored.
306+TEST_F(FqdnDhcpv6SrvTest, fqdnScrubbedEmpty) {
307+ // Create the query.
308+ Pkt6Ptr question = generateMessage(DHCPV6_SOLICIT, Option6ClientFqdn::FLAG_S,
309+ "___" , Option6ClientFqdn::FULL, true);
310+ ASSERT_TRUE(getClientFqdnOption(question));
311+ subnet_->setHostnameCharReplacement("");
312+
313+ // Create the response with an "assigned" lease.
314+ // Set the selected subnet so ddns params get returned correctly.
315+ AllocEngine::ClientContext6 ctx;
316+ ctx.subnet_ = subnet_;
317+ Pkt6Ptr answer = generateMessageWithIds(DHCPV6_ADVERTISE);
318+ addIA(1234, IOAddress("2001:db8:1::1"), answer, ctx);
319+
320+ // Process the client's FQDN.
321+ ASSERT_NO_THROW(srv_->processClientFqdn(question, answer, ctx));
322+
323+ // Should not have an FQDN option in the answer.
324+ EXPECT_FALSE(answer->getOption(D6O_CLIENT_FQDN));
325+ countFile("DHCP6_CLIENT_FQDN_SCRUBBED_EMPTY");
326+}
327+
328 } // end of anonymous namespace
329diff --git a/src/lib/dhcpsrv/d2_client_mgr.cc b/src/lib/dhcpsrv/d2_client_mgr.cc
330index 84ee11d9fb..54c815176e 100644
331--- a/src/lib/dhcpsrv/d2_client_mgr.cc
332+++ b/src/lib/dhcpsrv/d2_client_mgr.cc
333@@ -186,10 +186,15 @@ std::string
334 D2ClientMgr::qualifyName(const std::string& partial_name,
335 const DdnsParams& ddns_params,
336 const bool trailing_dot) const {
337+ if (partial_name.empty()) {
338+ isc_throw(BadValue, "D2ClientMgr::qualifyName"
339+ " - partial_name cannot be an empty string");
340+ }
341+
342 std::ostringstream gen_name;
343 gen_name << partial_name;
344 std::string suffix = ddns_params.getQualifyingSuffix();
345- if (!suffix.empty() && partial_name.back() != '.') {
346+ if (!suffix.empty() && (partial_name.back() != '.')) {
347 bool suffix_present = true;
348 std::string str = gen_name.str();
349 auto suffix_rit = suffix.rbegin();
350@@ -241,7 +246,7 @@ D2ClientMgr::qualifyName(const std::string& partial_name,
351 // If the trailing dot should not be appended but it is present,
352 // remove it.
353 if ((len > 0) && (str[len - 1] == '.')) {
354- gen_name.str(str.substr(0,len-1));
355+ gen_name.str(str.substr(0, len-1));
356 }
357
358 }
359diff --git a/src/lib/dhcpsrv/d2_client_mgr.h b/src/lib/dhcpsrv/d2_client_mgr.h
360index 7344f19a40..238fd0a415 100644
361--- a/src/lib/dhcpsrv/d2_client_mgr.h
362+++ b/src/lib/dhcpsrv/d2_client_mgr.h
363@@ -30,6 +30,14 @@
364 namespace isc {
365 namespace dhcp {
366
367+/// @brief Exception thrown when host name sanitizing reduces
368+/// the domain name to an empty string.
369+class FQDNScrubbedEmpty : public Exception {
370+public:
371+ FQDNScrubbedEmpty(const char* file, size_t line, const char* what) :
372+ isc::Exception(file, line, what) { }
373+};
374+
375 /// @brief Defines the type for D2 IO error handler.
376 /// This callback is invoked when a send to kea-dhcp-ddns completes with a
377 /// failed status. This provides the application layer (Kea) with a means to
378@@ -197,6 +205,7 @@ class D2ClientMgr : public dhcp_ddns::NameChangeSender::RequestSendHandler,
379 /// suffix itself is empty (i.e. "").
380 ///
381 /// @return std::string containing the qualified name.
382+ /// @throw BadValue if partial_name is empty.
383 std::string qualifyName(const std::string& partial_name,
384 const DdnsParams& ddns_params,
385 const bool trailing_dot) const;
386@@ -264,6 +273,9 @@ class D2ClientMgr : public dhcp_ddns::NameChangeSender::RequestSendHandler,
387 /// @param ddns_params DDNS behavioral configuration parameters
388 /// @tparam T FQDN Option class containing the FQDN data such as
389 /// dhcp::Option4ClientFqdn or dhcp::Option6ClientFqdn
390+ ///
391+ /// @throw FQDNScrubbedEmpty if hostname sanitizing reduces the input domain
392+ /// name to an empty string.
393 template <class T>
394 void adjustDomainName(const T& fqdn, T& fqdn_resp,
395 const DdnsParams& ddns_params);
396@@ -515,7 +527,12 @@ D2ClientMgr::adjustDomainName(const T& fqdn, T& fqdn_resp, const DdnsParams& ddn
397 ss << sanitizer->scrub(label);
398 }
399
400- client_name = ss.str();
401+ std::string clean_name = ss.str();
402+ if (clean_name.empty() || clean_name == ".") {
403+ isc_throw(FQDNScrubbedEmpty, client_name);
404+ }
405+
406+ client_name = clean_name;
407 }
408
409 // If the supplied name is partial, qualify it by adding the suffix.
410diff --git a/src/lib/dhcpsrv/tests/d2_client_unittest.cc b/src/lib/dhcpsrv/tests/d2_client_unittest.cc
411index 68ad2189d6..00375d0066 100644
412--- a/src/lib/dhcpsrv/tests/d2_client_unittest.cc
413+++ b/src/lib/dhcpsrv/tests/d2_client_unittest.cc
414@@ -9,6 +9,7 @@
415 #include <dhcp/option6_client_fqdn.h>
416 #include <dhcpsrv/d2_client_mgr.h>
417 #include <testutils/test_to_element.h>
418+#include <testutils/gtest_utils.h>
419 #include <exceptions/exceptions.h>
420 #include <util/str.h>
421
422@@ -627,6 +628,10 @@ TEST_F(D2ClientMgrParamsTest, qualifyName) {
423 qualified_name = mgr.qualifyName(partial_name, *ddns_params_, do_not_dot);
424 EXPECT_EQ("somehost.suffix.com", qualified_name);
425
426+ // Verify that an empty name throws.
427+ partial_name = "";
428+ ASSERT_THROW(mgr.qualifyName(partial_name, *ddns_params_, do_not_dot), BadValue);
429+
430 // Verify that an empty suffix and false flag, does not change the name
431 subnet_->setDdnsQualifyingSuffix("");
432 partial_name = "somehost";
433@@ -1257,4 +1262,41 @@ TEST_F(D2ClientMgrParamsTest, sanitizeFqdnV6) {
434 }
435 }
436
437+/// @brief Tests adjustDomainName template method with Option4ClientFqdn
438+/// when sanitizing scrubs input name empty.
439+TEST_F(D2ClientMgrParamsTest, adjustDomainNameV4ScrubbedEmpty) {
440+ D2ClientMgr mgr;
441+
442+ // Create enabled configuration
443+ subnet_->setDdnsSendUpdates(false);
444+ subnet_->setDdnsQualifyingSuffix("suffix.com");
445+ subnet_->setHostnameCharSet("[^A-Za-z0-9.-]");
446+ subnet_->setHostnameCharReplacement("");
447+
448+ Option4ClientFqdn request(0, Option4ClientFqdn::RCODE_CLIENT(),
449+ "___", Option4ClientFqdn::FULL);
450+
451+ Option4ClientFqdn response(request);
452+ ASSERT_THROW_MSG(mgr.adjustDomainName<Option4ClientFqdn>(request, response, *ddns_params_),
453+ FQDNScrubbedEmpty, "___.");
454+}
455+
456+/// @brief Tests adjustDomainName template method with Option4ClientFqdn
457+/// when sanitizing scrubs input name empty.
458+TEST_F(D2ClientMgrParamsTest, adjustDomainNameV6ScrubbedEmpty) {
459+ D2ClientMgr mgr;
460+
461+ // Create enabled configuration
462+ subnet_->setDdnsSendUpdates(false);
463+ subnet_->setDdnsQualifyingSuffix("suffix.com");
464+ subnet_->setHostnameCharSet("[^A-Za-z0-9.-]");
465+ subnet_->setHostnameCharReplacement("");
466+
467+ Option6ClientFqdn request(0, "___", Option6ClientFqdn::FULL);
468+
469+ Option6ClientFqdn response(request);
470+ ASSERT_THROW_MSG(mgr.adjustDomainName<Option6ClientFqdn>(request, response, *ddns_params_),
471+ FQDNScrubbedEmpty, "___.");
472+}
473+
474 } // end of anonymous namespace
diff --git a/meta/recipes-connectivity/kea/kea_3.0.1.bb b/meta/recipes-connectivity/kea/kea_3.0.1.bb
index 4a6623f94a..8729b1162e 100644
--- a/meta/recipes-connectivity/kea/kea_3.0.1.bb
+++ b/meta/recipes-connectivity/kea/kea_3.0.1.bb
@@ -21,6 +21,7 @@ SRC_URI = "http://ftp.isc.org/isc/kea/${PV}/${BP}.tar.xz \
21 file://0001-meson-use-a-runtime-safe-interpreter-string.patch \ 21 file://0001-meson-use-a-runtime-safe-interpreter-string.patch \
22 file://0001-mk_cfgrpt.sh-strip-prefixes.patch \ 22 file://0001-mk_cfgrpt.sh-strip-prefixes.patch \
23 file://0001-d2-dhcp-46-radius-dhcpsrv-Avoid-Boost-lexical_cast-o.patch \ 23 file://0001-d2-dhcp-46-radius-dhcpsrv-Avoid-Boost-lexical_cast-o.patch \
24 file://CVE-2025-11232.patch \
24 " 25 "
25SRC_URI[sha256sum] = "ec84fec4bb7f6b9d15a82e755a571e9348eb4d6fbc62bb3f6f1296cd7a24c566" 26SRC_URI[sha256sum] = "ec84fec4bb7f6b9d15a82e755a571e9348eb4d6fbc62bb3f6f1296cd7a24c566"
26 27