From: Thomas Markwalder Date: Mon, 20 Jul 2026 17:47:28 +0000 (-0400) Subject: [#4557] Log and drop malformed names X-Git-Tag: Kea-3.3.0~37 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8593cd1b9b94cfb9eae719e08df5384b443aa419;p=thirdparty%2Fkea.git [#4557] Log and drop malformed names 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 --- 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 index 0000000000..271a8f8460 --- /dev/null +++ b/changelog_unreleased/4557-f-153-kea-dhcp4-post-scrub-hostname-with-empty-dns-label-bypasses-cve-2025-11232-fix-causes @@ -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) diff --git a/src/bin/dhcp4/dhcp4_messages.cc b/src/bin/dhcp4/dhcp4_messages.cc index fe1378bf88..685cb17695 100644 --- a/src/bin/dhcp4/dhcp4_messages.cc +++ b/src/bin/dhcp4/dhcp4_messages.cc @@ -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.", diff --git a/src/bin/dhcp4/dhcp4_messages.h b/src/bin/dhcp4/dhcp4_messages.h index ab8669a31b..0d5465b9de 100644 --- a/src/bin/dhcp4/dhcp4_messages.h +++ b/src/bin/dhcp4/dhcp4_messages.h @@ -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; diff --git a/src/bin/dhcp4/dhcp4_messages.mes b/src/bin/dhcp4/dhcp4_messages.mes index fb750c1108..b580a60cc7 100644 --- a/src/bin/dhcp4/dhcp4_messages.mes +++ b/src/bin/dhcp4/dhcp4_messages.mes @@ -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. diff --git a/src/bin/dhcp4/dhcp4_srv.cc b/src/bin/dhcp4/dhcp4_srv.cc index 2f1d1af7ad..8a57df64f4 100644 --- a/src/bin/dhcp4/dhcp4_srv.cc +++ b/src/bin/dhcp4/dhcp4_srv.cc @@ -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; } diff --git a/src/bin/dhcp4/tests/fqdn_unittest.cc b/src/bin/dhcp4/tests/fqdn_unittest.cc index edec01e036..5336a4b56e 100644 --- a/src/bin/dhcp4/tests/fqdn_unittest.cc +++ b/src/bin/dhcp4/tests/fqdn_unittest.cc @@ -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(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 diff --git a/src/lib/dhcpsrv/d2_client_mgr.h b/src/lib/dhcpsrv/d2_client_mgr.h index 238fd0a415..b51f110d27 100644 --- a/src/lib/dhcpsrv/d2_client_mgr.h +++ b/src/lib/dhcpsrv/d2_client_mgr.h @@ -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); } diff --git a/src/lib/dhcpsrv/tests/d2_client_unittest.cc b/src/lib/dhcpsrv/tests/d2_client_unittest.cc index 4684d395bd..e399263ad9 100644 --- a/src/lib/dhcpsrv/tests/d2_client_unittest.cc +++ b/src/lib/dhcpsrv/tests/d2_client_unittest.cc @@ -1173,7 +1173,8 @@ TEST_F(D2ClientMgrParamsTest, sanitizeFqdnV4) { scenario.client_name_, scenario.name_type_); Option4ClientFqdn response(request); - mgr.adjustDomainName(request, response, *ddns_params_); + ASSERT_NO_THROW_LOG(mgr.adjustDomainName + (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(request, response, *ddns_params_); + ASSERT_NO_THROW_LOG(mgr.adjustDomainName + (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(request, response, *ddns_params_), FQDNScrubbedEmpty, "___."); + + Option4ClientFqdn request2(0, Option4ClientFqdn::RCODE_CLIENT(), + "aaa.___.bbb", Option4ClientFqdn::FULL); + + Option4ClientFqdn response2(request2); + ASSERT_THROW_MSG(mgr.adjustDomainName(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(request, response, *ddns_params_), FQDNScrubbedEmpty, "___."); + + Option6ClientFqdn request2(0, "aaa.___.bbb", Option6ClientFqdn::FULL); + + Option6ClientFqdn response2(request2); + ASSERT_THROW_MSG(mgr.adjustDomainName(request2, response2, *ddns_params_), + FQDNScrubbedEmpty, "aaa.___.bbb."); } } // end of anonymous namespace