]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#4557] Log and drop malformed names
authorThomas Markwalder <tmark@isc.org>
Mon, 20 Jul 2026 17:47:28 +0000 (13:47 -0400)
committerThomas Markwalder <tmark@isc.org>
Tue, 21 Jul 2026 16:46:53 +0000 (16:46 +0000)
new file:   changelog_unreleased/4557-f-153-kea-dhcp4-post-scrub-hostname-with-empty-dns-label-bypasses-cve-2025-11232-fix-causes
modified:   src/bin/dhcp4/dhcp4_messages.cc
modified:   src/bin/dhcp4/dhcp4_messages.h
modified:   src/bin/dhcp4/dhcp4_messages.mes
modified:   src/bin/dhcp4/dhcp4_srv.cc
modified:   src/bin/dhcp4/tests/fqdn_unittest.cc
modified:   src/lib/dhcpsrv/d2_client_mgr.h
modified:   src/lib/dhcpsrv/tests/d2_client_unittest.cc

changelog_unreleased/4557-f-153-kea-dhcp4-post-scrub-hostname-with-empty-dns-label-bypasses-cve-2025-11232-fix-causes [new file with mode: 0644]
src/bin/dhcp4/dhcp4_messages.cc
src/bin/dhcp4/dhcp4_messages.h
src/bin/dhcp4/dhcp4_messages.mes
src/bin/dhcp4/dhcp4_srv.cc
src/bin/dhcp4/tests/fqdn_unittest.cc
src/lib/dhcpsrv/d2_client_mgr.h
src/lib/dhcpsrv/tests/d2_client_unittest.cc

diff --git a/changelog_unreleased/4557-f-153-kea-dhcp4-post-scrub-hostname-with-empty-dns-label-bypasses-cve-2025-11232-fix-causes b/changelog_unreleased/4557-f-153-kea-dhcp4-post-scrub-hostname-with-empty-dns-label-bypasses-cve-2025-11232-fix-causes
new file mode 100644 (file)
index 0000000..271a8f8
--- /dev/null
@@ -0,0 +1,8 @@
+[bug]          tmark
+       Kea now logs and drops host names and FQDNs
+       when host name sanitization results in
+       embedded empty labels (e.g. "aaa..bbb.com").
+       Applies to both kea-dhcp4 and kea-dhcp6.
+       Thank you to Qifan Zhang from Palo Alto Networks
+       for reporting the issue.
+       (Gitlab #4557)
index fe1378bf886f39cfe410d0dce8a7717a84f62537..685cb17695043700e26e10fccd140166b0eaf0e5 100644 (file)
@@ -167,6 +167,7 @@ extern const isc::log::MessageID DHCP4_RESPONSE_FQDN_DATA = "DHCP4_RESPONSE_FQDN
 extern const isc::log::MessageID DHCP4_RESPONSE_HOSTNAME_DATA = "DHCP4_RESPONSE_HOSTNAME_DATA";
 extern const isc::log::MessageID DHCP4_RESPONSE_HOSTNAME_GENERATE = "DHCP4_RESPONSE_HOSTNAME_GENERATE";
 extern const isc::log::MessageID DHCP4_ROOT_USER_SECURITY_WARNING = "DHCP4_ROOT_USER_SECURITY_WARNING";
+extern const isc::log::MessageID DHCP4_SANITIZED_HOSTNAME_MALFORMED = "DHCP4_SANITIZED_HOSTNAME_MALFORMED";
 extern const isc::log::MessageID DHCP4_SECURITY_CHECKS_DISABLED = "DHCP4_SECURITY_CHECKS_DISABLED";
 extern const isc::log::MessageID DHCP4_SERVER_FAILED = "DHCP4_SERVER_FAILED";
 extern const isc::log::MessageID DHCP4_SERVER_INITIATED_DECLINE = "DHCP4_SERVER_INITIATED_DECLINE";
@@ -357,6 +358,7 @@ const char* values[] = {
     "DHCP4_RESPONSE_HOSTNAME_DATA", "%1: including Hostname option in the server's response: %2",
     "DHCP4_RESPONSE_HOSTNAME_GENERATE", "%1: server has generated hostname %2 for the client",
     "DHCP4_ROOT_USER_SECURITY_WARNING", "kea-dhcp4 running as root user!",
+    "DHCP4_SANITIZED_HOSTNAME_MALFORMED", "%1: hostname after sanitizing is malformed: %2",
     "DHCP4_SECURITY_CHECKS_DISABLED", "Invoked with command line option -X, Security checks are disabled!!",
     "DHCP4_SERVER_FAILED", "server failed: %1",
     "DHCP4_SERVER_INITIATED_DECLINE", "%1: Lease for addr %2 has been found to be already in use. The lease will be unavailable for %3 seconds.",
index ab8669a31b294a670a2cd1c92f68e427fd39b709..0d5465b9def5687a7b91875489ea0a2891f2717e 100644 (file)
@@ -168,6 +168,7 @@ extern const isc::log::MessageID DHCP4_RESPONSE_FQDN_DATA;
 extern const isc::log::MessageID DHCP4_RESPONSE_HOSTNAME_DATA;
 extern const isc::log::MessageID DHCP4_RESPONSE_HOSTNAME_GENERATE;
 extern const isc::log::MessageID DHCP4_ROOT_USER_SECURITY_WARNING;
+extern const isc::log::MessageID DHCP4_SANITIZED_HOSTNAME_MALFORMED;
 extern const isc::log::MessageID DHCP4_SECURITY_CHECKS_DISABLED;
 extern const isc::log::MessageID DHCP4_SERVER_FAILED;
 extern const isc::log::MessageID DHCP4_SERVER_INITIATED_DECLINE;
index fb750c1108c5cf02fdda68e6e41b791363320ef4..b580a60cc7d98d9f7c5a982e3d88df15646c57a1 100644 (file)
@@ -1255,3 +1255,10 @@ expected: the erroneous response is dropped, the request query is displayed.
 An DHCPOFFER for the 0.0.0.0 address was generated for a client requesting
 the v6-only-preferred (108) option but the option is not in the response as
 expected: the erroneous response is dropped, the discover query is displayed.
+
+% DHCP4_SANITIZED_HOSTNAME_MALFORMED %1: hostname after sanitizing is malformed: %2
+Logged at debug log level 50.
+This debug message is issued when the value generated by the server after applying
+host name sanitization does not constitute a valid domain name.  The first argument
+includes the client and transaction identification information. The second argument
+contains a description of the data error.
index 2f1d1af7ad1860341615dbdd611770441795ced6..8a57df64f481f0d3bf1158304330d9e93e3ba3a0 100644 (file)
@@ -2976,6 +2976,15 @@ Dhcpv4Srv::processHostnameOption(Dhcpv4Exchange& ex) {
                 return;
             }
 
+            try {
+                label_count = OptionDataTypeUtil::getLabelCount(tmp);
+            } catch (const std::exception& exc) {
+                LOG_DEBUG(ddns4_logger, DBG_DHCP4_DETAIL, DHCP4_SANITIZED_HOSTNAME_MALFORMED)
+                    .arg(ex.getQuery()->getLabel())
+                    .arg(exc.what());
+                return;
+            }
+
             hostname = tmp;
         }
 
index edec01e036e1c1db1b85969d10ac2dc0490e29ab..5336a4b56ed2f115568cc6a8401d541ce2ffe3b0 100644 (file)
@@ -3204,6 +3204,22 @@ TEST_F(NameDhcpv4SrvTest, hostnameScrubbedEmpty) {
 
     // Hostname should not be in the response.
     ASSERT_FALSE(resp->getOption(DHO_HOST_NAME));
+
+    // Set the hostname option.
+    ASSERT_NO_THROW(client.includeHostname("aaa.___.bbb"));
+
+    // Send the DHCPDISCOVER and make sure that the server responded.
+    ASSERT_NO_THROW(client.doDiscover());
+    resp = client.getContext().response_;
+    ASSERT_TRUE(resp);
+    ASSERT_EQ(DHCPOFFER, static_cast<int>(resp->getType()));
+
+    // Should have logged that it was scrubbed empty.
+    log = "DHCP4_SANITIZED_HOSTNAME_MALFORMED";
+    EXPECT_EQ(1U, countFile(log));
+
+    // Hostname should not be in the response.
+    ASSERT_FALSE(resp->getOption(DHO_HOST_NAME));
 }
 
 // Verifies that when the FQDN option is scrubbed empty it is logged
@@ -3232,5 +3248,4 @@ TEST_F(NameDhcpv4SrvTest, fqdnScrubbedEmpty) {
     ASSERT_FALSE(resp->getOption(DHO_FQDN));
 }
 
-
 } // end of anonymous namespace
index 238fd0a41580bc1ac520b3e2240b356aaafdf686..b51f110d27d8e37d39acd00d88b4ac18f1bb7ffe 100644 (file)
@@ -528,7 +528,8 @@ D2ClientMgr::adjustDomainName(const T& fqdn, T& fqdn_resp, const DdnsParams& ddn
             }
 
             std::string clean_name = ss.str();
-            if (clean_name.empty() || clean_name == ".") {
+            if (clean_name.empty() || clean_name == "." ||
+               (clean_name.find("..") != std::string::npos)) {
                 isc_throw(FQDNScrubbedEmpty, client_name);
             }
 
index 4684d395bdfac0768f94ee177025adf84dedd6de..e399263ad9540dd43d1a051689fb3a7e2a50f99a 100644 (file)
@@ -1173,7 +1173,8 @@ TEST_F(D2ClientMgrParamsTest, sanitizeFqdnV4) {
                                       scenario.client_name_, scenario.name_type_);
             Option4ClientFqdn response(request);
 
-            mgr.adjustDomainName<Option4ClientFqdn>(request, response, *ddns_params_);
+            ASSERT_NO_THROW_LOG(mgr.adjustDomainName<Option4ClientFqdn>
+                                (request, response, *ddns_params_));
             EXPECT_EQ(scenario.expected_name_, response.getDomainName());
             EXPECT_EQ(Option4ClientFqdn::FULL, response.getDomainNameType());
         }
@@ -1258,7 +1259,8 @@ TEST_F(D2ClientMgrParamsTest, sanitizeFqdnV6) {
             Option6ClientFqdn request(0, scenario.client_name_, scenario.name_type_);
             Option6ClientFqdn response(request);
 
-            mgr.adjustDomainName<Option6ClientFqdn>(request, response, *ddns_params_);
+            ASSERT_NO_THROW_LOG(mgr.adjustDomainName<Option6ClientFqdn>
+                                (request, response, *ddns_params_));
             EXPECT_EQ(scenario.expected_name_, response.getDomainName());
             EXPECT_EQ(Option6ClientFqdn::FULL, response.getDomainNameType());
         }
@@ -1282,6 +1284,13 @@ TEST_F(D2ClientMgrParamsTest, adjustDomainNameV4ScrubbedEmpty) {
     Option4ClientFqdn response(request);
     ASSERT_THROW_MSG(mgr.adjustDomainName<Option4ClientFqdn>(request, response, *ddns_params_),
                      FQDNScrubbedEmpty, "___.");
+
+    Option4ClientFqdn request2(0, Option4ClientFqdn::RCODE_CLIENT(),
+                              "aaa.___.bbb", Option4ClientFqdn::FULL);
+
+    Option4ClientFqdn response2(request2);
+    ASSERT_THROW_MSG(mgr.adjustDomainName<Option4ClientFqdn>(request2, response2, *ddns_params_),
+                     FQDNScrubbedEmpty, "aaa.___.bbb.");
 }
 
 /// @brief Tests adjustDomainName template method with Option4ClientFqdn
@@ -1300,6 +1309,12 @@ TEST_F(D2ClientMgrParamsTest, adjustDomainNameV6ScrubbedEmpty) {
     Option6ClientFqdn response(request);
     ASSERT_THROW_MSG(mgr.adjustDomainName<Option6ClientFqdn>(request, response, *ddns_params_),
                      FQDNScrubbedEmpty, "___.");
+
+    Option6ClientFqdn request2(0, "aaa.___.bbb", Option6ClientFqdn::FULL);
+
+    Option6ClientFqdn response2(request2);
+    ASSERT_THROW_MSG(mgr.adjustDomainName<Option6ClientFqdn>(request2, response2, *ddns_params_),
+                     FQDNScrubbedEmpty, "aaa.___.bbb.");
 }
 
 } // end of anonymous namespace