Version in base suite: 1.9.14-0+deb13u1 Base version: dnsdist_1.9.14-0+deb13u1 Target version: dnsdist_1.9.15-0+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/d/dnsdist/dnsdist_1.9.14-0+deb13u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/d/dnsdist/dnsdist_1.9.15-0+deb13u1.dsc configure | 20 +-- configure.ac | 2 debian/changelog | 8 + dnsdist-crypto.cc | 3 dnsdist-doh-common.cc | 91 ++++++++------- dnsdist-ecs.cc | 8 - dnsdist-lua-actions.cc | 5 dnsdist-tcp-downstream.cc | 6 - dnsdist-web.cc | 27 ++++ dnsdist.1 | 2 doh3.cc | 248 +++++++++++++++++++++++------------------- ext/yahttp/yahttp/reqresp.cpp | 7 + test-base64_cc.cc | 12 ++ test-dnsdist_cc.cc | 103 +++++++++++++++++ 14 files changed, 366 insertions(+), 176 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpj3fdkzh8/dnsdist_1.9.14-0+deb13u1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpj3fdkzh8/dnsdist_1.9.15-0+deb13u1.dsc: no acceptable signature found diff -Nru dnsdist-1.9.14/configure dnsdist-1.9.15/configure --- dnsdist-1.9.14/configure 2026-04-22 16:47:25.000000000 +0000 +++ dnsdist-1.9.15/configure 2026-06-09 07:55:41.000000000 +0000 @@ -1,6 +1,6 @@ #! /bin/sh # Guess values for system-dependent variables and create Makefiles. -# Generated by GNU Autoconf 2.71 for dnsdist 1.9.14. +# Generated by GNU Autoconf 2.71 for dnsdist 1.9.15. # # # Copyright (C) 1992-1996, 1998-2017, 2020-2021 Free Software Foundation, @@ -618,8 +618,8 @@ # Identity of this package. PACKAGE_NAME='dnsdist' PACKAGE_TARNAME='dnsdist' -PACKAGE_VERSION='1.9.14' -PACKAGE_STRING='dnsdist 1.9.14' +PACKAGE_VERSION='1.9.15' +PACKAGE_STRING='dnsdist 1.9.15' PACKAGE_BUGREPORT='' PACKAGE_URL='' @@ -1645,7 +1645,7 @@ # Omit some internal or obsolete options to make the list less imposing. # This message is too long to be a string in the A/UX 3.1 sh. cat <<_ACEOF -\`configure' configures dnsdist 1.9.14 to adapt to many kinds of systems. +\`configure' configures dnsdist 1.9.15 to adapt to many kinds of systems. Usage: $0 [OPTION]... [VAR=VALUE]... @@ -1716,7 +1716,7 @@ if test -n "$ac_init_help"; then case $ac_init_help in - short | recursive ) echo "Configuration of dnsdist 1.9.14:";; + short | recursive ) echo "Configuration of dnsdist 1.9.15:";; esac cat <<\_ACEOF @@ -1951,7 +1951,7 @@ test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then cat <<\_ACEOF -dnsdist configure 1.9.14 +dnsdist configure 1.9.15 generated by GNU Autoconf 2.71 Copyright (C) 2021 Free Software Foundation, Inc. @@ -2440,7 +2440,7 @@ This file contains any messages produced by compilers while running configure, to aid debugging if configure makes a mistake. -It was created by dnsdist $as_me 1.9.14, which was +It was created by dnsdist $as_me 1.9.15, which was generated by GNU Autoconf 2.71. Invocation command line was $ $0$ac_configure_args_raw @@ -3932,7 +3932,7 @@ # Define the identity of the package. PACKAGE='dnsdist' - VERSION='1.9.14' + VERSION='1.9.15' printf "%s\n" "#define PACKAGE \"$PACKAGE\"" >>confdefs.h @@ -28627,7 +28627,7 @@ # report actual input values of CONFIG_FILES etc. instead of their # values after options handling. ac_log=" -This file was extended by dnsdist $as_me 1.9.14, which was +This file was extended by dnsdist $as_me 1.9.15, which was generated by GNU Autoconf 2.71. Invocation command line was CONFIG_FILES = $CONFIG_FILES @@ -28695,7 +28695,7 @@ cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_cs_config='$ac_cs_config_escaped' ac_cs_version="\\ -dnsdist config.status 1.9.14 +dnsdist config.status 1.9.15 configured by $0, generated by GNU Autoconf 2.71, with options \\"\$ac_cs_config\\" diff -Nru dnsdist-1.9.14/configure.ac dnsdist-1.9.15/configure.ac --- dnsdist-1.9.14/configure.ac 2026-04-22 16:47:15.000000000 +0000 +++ dnsdist-1.9.15/configure.ac 2026-06-09 07:55:30.000000000 +0000 @@ -1,6 +1,6 @@ AC_PREREQ([2.69]) -AC_INIT([dnsdist], [1.9.14]) +AC_INIT([dnsdist], [1.9.15]) AM_INIT_AUTOMAKE([foreign tar-ustar dist-bzip2 no-dist-gzip parallel-tests 1.11 subdir-objects]) AM_SILENT_RULES([yes]) AC_CONFIG_MACRO_DIR([m4]) diff -Nru dnsdist-1.9.14/debian/changelog dnsdist-1.9.15/debian/changelog --- dnsdist-1.9.14/debian/changelog 2026-04-26 18:59:04.000000000 +0000 +++ dnsdist-1.9.15/debian/changelog 2026-06-16 22:17:43.000000000 +0000 @@ -1,3 +1,11 @@ +dnsdist (1.9.15-0+deb13u1) trixie-security; urgency=medium + + * New upstream version 1.9.15, fixing security issues + CVE-2026-40011, CVE-2026-42004, CVE-2026-42005, CVE-2026-40208, + CVE-2026-40209, CVE-2026-40210, CVE-2026-40211 + + -- Chris Hofstaedtler Wed, 17 Jun 2026 00:17:43 +0200 + dnsdist (1.9.14-0+deb13u1) trixie-security; urgency=medium * New upstream version 1.9.14, fixing (from 1.9.13) CVE-2026-33257, diff -Nru dnsdist-1.9.14/dnsdist-crypto.cc dnsdist-1.9.15/dnsdist-crypto.cc --- dnsdist-1.9.14/dnsdist-crypto.cc 2026-04-22 16:47:03.000000000 +0000 +++ dnsdist-1.9.15/dnsdist-crypto.cc 2026-06-09 07:55:15.000000000 +0000 @@ -452,6 +452,9 @@ } while (isspace(strInput.at(iInNum))) { iInNum++; + if (iInNum >= iInSize) { + return -1; + } } cChar = B64Decode1(strInput.at(iInNum++)); diff -Nru dnsdist-1.9.14/dnsdist-doh-common.cc dnsdist-1.9.15/dnsdist-doh-common.cc --- dnsdist-1.9.14/dnsdist-doh-common.cc 2026-04-22 16:47:03.000000000 +0000 +++ dnsdist-1.9.15/dnsdist-doh-common.cc 2026-06-09 07:55:15.000000000 +0000 @@ -142,59 +142,66 @@ { std::optional result{std::nullopt}; - if (path.size() <= 5) { - return result; - } + try { + if (path.size() <= 5) { + return result; + } - auto pos = path.find("?dns="); - if (pos == string::npos) { - pos = path.find("&dns="); - } + auto pos = path.find("?dns="); + if (pos == string::npos) { + pos = path.find("&dns="); + } - if (pos == string::npos) { - return result; - } + if (pos == string::npos) { + return result; + } - // need to base64url decode this - string sdns; - const size_t payloadSize = path.size() - pos - 5; - size_t neededPadding = 0; - switch (payloadSize % 4) { - case 2: - neededPadding = 2; - break; - case 3: - neededPadding = 1; - break; - } - sdns.reserve(payloadSize + neededPadding); - sdns = path.substr(pos + 5); - for (auto& entry : sdns) { - switch (entry) { - case '-': - entry = '+'; + // need to base64url decode this + string sdns; + const size_t payloadSize = path.size() - pos - 5; + size_t neededPadding = 0; + switch (payloadSize % 4) { + case 2: + neededPadding = 2; break; - case '_': - entry = '/'; + case 3: + neededPadding = 1; break; } - } + sdns.reserve(payloadSize + neededPadding); + sdns = path.substr(pos + 5); + for (auto& entry : sdns) { + switch (entry) { + case '-': + entry = '+'; + break; + case '_': + entry = '/'; + break; + } + } - if (neededPadding != 0) { - // re-add padding that may have been missing - sdns.append(neededPadding, '='); - } + if (neededPadding != 0) { + // re-add padding that may have been missing + sdns.append(neededPadding, '='); + } + + PacketBuffer decoded; + /* rough estimate so we hopefully don't need a new allocation later */ + /* We reserve at few additional bytes to be able to add EDNS later */ + const size_t estimate = ((sdns.size() * 3) / 4); + decoded.reserve(estimate); + if (B64Decode(sdns, decoded) < 0) { + return result; + } - PacketBuffer decoded; - /* rough estimate so we hopefully don't need a new allocation later */ - /* We reserve at few additional bytes to be able to add EDNS later */ - const size_t estimate = ((sdns.size() * 3) / 4); - decoded.reserve(estimate); - if (B64Decode(sdns, decoded) < 0) { + result = std::move(decoded); return result; } + catch (const std::exception& exp) { + infolog("Exception while decoding base64 payload: %s", exp.what()); + } - result = std::move(decoded); return result; } } diff -Nru dnsdist-1.9.14/dnsdist-ecs.cc dnsdist-1.9.15/dnsdist-ecs.cc --- dnsdist-1.9.14/dnsdist-ecs.cc 2026-04-22 16:47:03.000000000 +0000 +++ dnsdist-1.9.15/dnsdist-ecs.cc 2026-06-09 07:55:15.000000000 +0000 @@ -117,7 +117,7 @@ rrname = pr.getName(); pr.getDnsrecordheader(ah); - if (ah.d_type != QType::OPT) { + if (!rrname.isRoot() || ah.d_type != QType::OPT) { pw.startRecord(rrname, ah.d_type, ah.d_ttl, ah.d_class, DNSResourceRecord::ADDITIONAL, true); pr.xfrBlob(blob); pw.xfrBlob(blob); @@ -231,7 +231,7 @@ rrname = pr.getName(); pr.getDnsrecordheader(ah); - if (ah.d_type != QType::OPT) { + if (!rrname.isRoot() || ah.d_type != QType::OPT) { pw.startRecord(rrname, ah.d_type, ah.d_ttl, ah.d_class, DNSResourceRecord::ADDITIONAL, true); pr.xfrBlob(blob); pw.xfrBlob(blob); @@ -367,7 +367,7 @@ rrname = pr.getName(); pr.getDnsrecordheader(ah); - if (ah.d_type == QType::OPT) { + if (rrname.isRoot() && ah.d_type == QType::OPT) { *optStart = start; *optLen = (pr.getPosition() - start) + ah.d_clen; @@ -830,7 +830,7 @@ rrname = pr.getName(); pr.getDnsrecordheader(ah); - if (ah.d_type != QType::OPT) { + if (!rrname.isRoot() || ah.d_type != QType::OPT) { pw.startRecord(rrname, ah.d_type, ah.d_ttl, ah.d_class, DNSResourceRecord::ADDITIONAL, true); pr.xfrBlob(blob); pw.xfrBlob(blob); diff -Nru dnsdist-1.9.14/dnsdist-lua-actions.cc dnsdist-1.9.15/dnsdist-lua-actions.cc --- dnsdist-1.9.14/dnsdist-lua-actions.cc 2026-04-22 16:47:03.000000000 +0000 +++ dnsdist-1.9.15/dnsdist-lua-actions.cc 2026-06-09 07:55:15.000000000 +0000 @@ -1029,9 +1029,10 @@ return Action::None; } - std::string optRData; // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - generateEDNSOption(d_code, reinterpret_cast(mac.data()), optRData); + std::string macStr(reinterpret_cast(mac.data()), mac.size()); + std::string optRData; + generateEDNSOption(d_code, macStr, optRData); if (dnsquestion->getHeader()->arcount > 0) { bool ednsAdded = false; diff -Nru dnsdist-1.9.14/dnsdist-tcp-downstream.cc dnsdist-1.9.15/dnsdist-tcp-downstream.cc --- dnsdist-1.9.14/dnsdist-tcp-downstream.cc 2026-04-22 16:47:03.000000000 +0000 +++ dnsdist-1.9.15/dnsdist-tcp-downstream.cc 2026-06-09 07:55:15.000000000 +0000 @@ -200,6 +200,10 @@ DEBUGLOG("Exception when parsing IXFR TCP Query to DNS: " << e.what()); /* ponder what to do here, shall we close the connection? */ } + catch (const std::exception& exp) { + DEBUGLOG("Exception when parsing IXFR TCP Query to DNS: " << exp.what()); + /* ponder what to do here, shall we close the connection? */ + } return false; } @@ -528,13 +532,13 @@ // start sending the query if (d_state == State::idle || d_state == State::waitingForResponseFromBackend) { DEBUGLOG("Sending new query to backend right away, with ID "< int { - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): Quiche API - std::string_view key(reinterpret_cast(name), name_len); - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): Quiche API - std::string_view content(reinterpret_cast(value), value_len); - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): Quiche API - auto* headersptr = reinterpret_cast(argp); - if (headersptr->size() >= dnsdist::doh::MAX_INCOMING_HTTP_HEADERS) { - /* be nice but not too nice */ - return 1; - } - headersptr->emplace(key, content); - return 0; - }, - &headers); + try { + auto& headers = conn.d_headersBuffers[streamID]; + // Callback result. Any value other than 0 will interrupt further header processing. + int cbresult = quiche_h3_event_for_each_header( + event, + [](uint8_t* name, size_t name_len, uint8_t* value, size_t value_len, void* argp) -> int { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): Quiche API + std::string_view key(reinterpret_cast(name), name_len); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): Quiche API + std::string_view content(reinterpret_cast(value), value_len); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast): Quiche API + auto* headersptr = reinterpret_cast(argp); + if (headersptr->size() >= dnsdist::doh::MAX_INCOMING_HTTP_HEADERS) { + /* be nice but not too nice */ + return 1; + } + headersptr->emplace(key, content); + return 0; + }, + &headers); #ifdef DEBUGLOG_ENABLED - DEBUGLOG("Processed headers of stream " << streamID); - for (const auto& [key, value] : headers) { - DEBUGLOG(" " << key << ": " << value); - } -#endif - if (cbresult != 0 || headers.count(":method") == 0) { - handleImmediateError("Unable to process query headers"); - return; - } - - if (headers.at(":method") == "GET") { - if (headers.count(":path") == 0 || headers.at(":path").empty()) { - handleImmediateError("Path not found"); - return; + DEBUGLOG("Processed headers of stream " << streamID); + for (const auto& [key, value] : headers) { + DEBUGLOG(" " << key << ": " << value); } - const auto& path = headers.at(":path"); - auto payload = dnsdist::doh::getPayloadFromPath(path); - if (!payload) { - handleImmediateError("Unable to find the DNS parameter"); +#endif + if (cbresult != 0 || headers.count(":method") == 0) { + handleImmediateError("Unable to process query headers"); return; } - if (payload->size() < sizeof(dnsheader)) { - handleImmediateError("DoH3 non-compliant query"); + + if (headers.at(":method") == "GET") { + if (headers.count(":path") == 0 || headers.at(":path").empty()) { + handleImmediateError("Path not found"); + return; + } + const auto& path = headers.at(":path"); + auto payload = dnsdist::doh::getPayloadFromPath(path); + if (!payload) { + handleImmediateError("Unable to find the DNS parameter"); + return; + } + if (payload->size() < sizeof(dnsheader)) { + handleImmediateError("DoH3 non-compliant query"); + return; + } + DEBUGLOG("Dispatching GET query"); + doh3_dispatch_query(*(frontend.d_server_config), std::move(*payload), conn.d_localAddr, client, serverConnID, streamID); + conn.removeTemporaryQueryContent(streamID); return; } - DEBUGLOG("Dispatching GET query"); - doh3_dispatch_query(*(frontend.d_server_config), std::move(*payload), conn.d_localAddr, client, serverConnID, streamID); - conn.d_streamBuffers.erase(streamID); - conn.d_headersBuffers.erase(streamID); - return; - } - if (headers.at(":method") == "POST") { + if (headers.at(":method") == "POST") { #if defined(HAVE_QUICHE_H3_EVENT_HEADERS_HAS_MORE_FRAMES) - if (!quiche_h3_event_headers_has_more_frames(event)) { + if (!quiche_h3_event_headers_has_more_frames(event)) { #else - if (!quiche_h3_event_headers_has_body(event)) { + if (!quiche_h3_event_headers_has_body(event)) { #endif - handleImmediateError("Empty POST query"); + handleImmediateError("Empty POST query"); + } + return; } - return; - } - handleImmediateError("Unsupported HTTP method"); + handleImmediateError("Unsupported HTTP method"); + } + catch (const std::exception& exp) { + handleImmediateError("Exception while processing query"); + throw; + } } static void processH3DataEvent(ClientState& clientState, DOH3Frontend& frontend, H3Connection& conn, const ComboAddress& client, const PacketBuffer& serverConnID, const uint64_t streamID, quiche_h3_event* event, PacketBuffer& buffer) @@ -791,57 +801,69 @@ ++clientState.nonCompliantQueries; ++frontend.d_errorResponses; h3_send_response(conn, streamID, 400, msg); + conn.removeTemporaryQueryContent(streamID); }; - auto& headers = conn.d_headersBuffers.at(streamID); - - if (headers.at(":method") != "POST") { - handleImmediateError("DATA frame for non-POST method"); - return; - } - if (headers.count("content-type") == 0 || headers.at("content-type") != "application/dns-message") { - handleImmediateError("Unsupported content-type"); - return; - } + try { + auto headersIt = conn.d_headersBuffers.find(streamID); + if (headersIt == conn.d_headersBuffers.end()) { + handleImmediateError("DATA frame for stream without headers"); + return; + } + auto& headers = headersIt->second; + { + if (auto methodIt = headers.find(":method"); methodIt == headers.end() || methodIt->second != "POST") { + handleImmediateError("DATA frame for non-POST method"); + return; + } + } - buffer.resize(std::numeric_limits::max()); - auto& streamBuffer = conn.d_streamBuffers[streamID]; + if (auto contentTypeHeaderIt = headers.find("content-type"); contentTypeHeaderIt == headers.end() || contentTypeHeaderIt->second != "application/dns-message") { + handleImmediateError("Unsupported content-type"); + return; + } - while (true) { buffer.resize(std::numeric_limits::max()); - ssize_t len = quiche_h3_recv_body(conn.d_http3.get(), - conn.d_conn.get(), streamID, - buffer.data(), buffer.size()); + auto& streamBuffer = conn.d_streamBuffers[streamID]; - if (len <= 0) { - break; + while (true) { + buffer.resize(std::numeric_limits::max()); + ssize_t len = quiche_h3_recv_body(conn.d_http3.get(), + conn.d_conn.get(), streamID, + buffer.data(), buffer.size()); + + if (len <= 0) { + break; + } + + if (len > std::numeric_limits::max() || (std::numeric_limits::max() - streamBuffer.size()) < static_cast(len)) { + vinfolog("DOH3 data frame of size %d is too large for a DNS query (we already have %d)", len, streamBuffer.size()); + conn.d_streamBuffers.erase(streamID); + handleImmediateError("DoH3 non-compliant query"); + return; + } + + buffer.resize(static_cast(len)); + streamBuffer.insert(streamBuffer.end(), buffer.begin(), buffer.end()); } - if (len > std::numeric_limits::max() || (std::numeric_limits::max() - streamBuffer.size()) < static_cast(len)) { - vinfolog("DOH3 data frame of size %d is too large for a DNS query (we already have %d)", len, streamBuffer.size()); - conn.d_streamBuffers.erase(streamID); - handleImmediateError("DoH3 non-compliant query"); + if (!quiche_conn_stream_finished(conn.d_conn.get(), streamID)) { return; } - buffer.resize(static_cast(len)); - streamBuffer.insert(streamBuffer.end(), buffer.begin(), buffer.end()); - } + if (streamBuffer.size() < sizeof(dnsheader)) { + handleImmediateError("DoH3 non-compliant query"); + return; + } - if (!quiche_conn_stream_finished(conn.d_conn.get(), streamID)) { - return; + DEBUGLOG("Dispatching POST query"); + doh3_dispatch_query(*(frontend.d_server_config), std::move(streamBuffer), conn.d_localAddr, client, serverConnID, streamID); + conn.removeTemporaryQueryContent(streamID); } - - if (streamBuffer.size() < sizeof(dnsheader)) { - conn.d_streamBuffers.erase(streamID); - handleImmediateError("DoH3 non-compliant query"); - return; + catch (const std::exception& exp) { + handleImmediateError("Exception while processing query"); + throw; } - - DEBUGLOG("Dispatching POST query"); - doh3_dispatch_query(*(frontend.d_server_config), std::move(streamBuffer), conn.d_localAddr, client, serverConnID, streamID); - conn.d_headersBuffers.erase(streamID); - conn.d_streamBuffers.erase(streamID); } static void processH3Events(ClientState& clientState, DOH3Frontend& frontend, H3Connection& conn, const ComboAddress& client, const PacketBuffer& serverConnID, PacketBuffer& buffer) @@ -856,27 +878,31 @@ if (streamID < 0) { break; } - std::unique_ptr eventPtr(event, quiche_h3_event_free); - event = nullptr; - conn.d_headersBuffers.try_emplace(streamID, h3_headers_t{}); - - switch (quiche_h3_event_type(eventPtr.get())) { - case QUICHE_H3_EVENT_HEADERS: { - processH3HeaderEvent(clientState, frontend, conn, client, serverConnID, streamID, eventPtr.get()); - break; - } - case QUICHE_H3_EVENT_DATA: { - processH3DataEvent(clientState, frontend, conn, client, serverConnID, streamID, eventPtr.get(), buffer); - break; + + try { + std::unique_ptr eventPtr(event, quiche_h3_event_free); + event = nullptr; + + switch (quiche_h3_event_type(eventPtr.get())) { + case QUICHE_H3_EVENT_HEADERS: { + processH3HeaderEvent(clientState, frontend, conn, client, serverConnID, streamID, eventPtr.get()); + break; + } + case QUICHE_H3_EVENT_DATA: { + processH3DataEvent(clientState, frontend, conn, client, serverConnID, streamID, eventPtr.get(), buffer); + break; + } + case QUICHE_H3_EVENT_FINISHED: + case QUICHE_H3_EVENT_RESET: + conn.removeTemporaryQueryContent(streamID); + break; + case QUICHE_H3_EVENT_PRIORITY_UPDATE: + case QUICHE_H3_EVENT_GOAWAY: + break; + } } - case QUICHE_H3_EVENT_FINISHED: - case QUICHE_H3_EVENT_RESET: - conn.d_headersBuffers.erase(streamID); - conn.d_streamBuffers.erase(streamID); - break; - case QUICHE_H3_EVENT_GOAWAY: - case QUICHE_H3_EVENT_PRIORITY_UPDATE: - break; + catch (const std::exception& exp) { + infolog("Error processing DoH3 event for stream %d: %s", streamID, exp.what()); } } } diff -Nru dnsdist-1.9.14/ext/yahttp/yahttp/reqresp.cpp dnsdist-1.9.15/ext/yahttp/yahttp/reqresp.cpp --- dnsdist-1.9.14/ext/yahttp/yahttp/reqresp.cpp 2026-04-22 16:47:02.000000000 +0000 +++ dnsdist-1.9.15/ext/yahttp/yahttp/reqresp.cpp 2026-06-09 07:55:15.000000000 +0000 @@ -181,7 +181,12 @@ if (chunk_size == 0) { char buf[100]; // read chunk length - if ((pos = buffer.find('\n')) == std::string::npos) return false; + if ((pos = buffer.find('\n')) == std::string::npos) { + if (buffer.size() > 99) { + throw ParseError("Nonsensical chunk_size"); + } + return false; + } if (pos > 99) throw ParseError("Impossible chunk_size"); buffer.copy(buf, pos); diff -Nru dnsdist-1.9.14/test-base64_cc.cc dnsdist-1.9.15/test-base64_cc.cc --- dnsdist-1.9.14/test-base64_cc.cc 2026-04-22 16:47:03.000000000 +0000 +++ dnsdist-1.9.15/test-base64_cc.cc 2026-06-09 07:55:15.000000000 +0000 @@ -77,4 +77,16 @@ } +BOOST_AUTO_TEST_CASE(test_Base64_Decode_Trailing_Whitespace) +{ + const std::string trailing("eg== "); + std::string decoded; + auto ret = B64Decode(trailing, decoded); +#if defined(DNSDIST) + BOOST_CHECK_EQUAL(ret, -1); +#else + BOOST_CHECK(ret == 0); +#endif +} + BOOST_AUTO_TEST_SUITE_END() diff -Nru dnsdist-1.9.14/test-dnsdist_cc.cc dnsdist-1.9.15/test-dnsdist_cc.cc --- dnsdist-1.9.14/test-dnsdist_cc.cc 2026-04-22 16:47:03.000000000 +0000 +++ dnsdist-1.9.15/test-dnsdist_cc.cc 2026-06-09 07:55:15.000000000 +0000 @@ -128,19 +128,41 @@ BOOST_CHECK_EQUAL(expectedOption.substr(EDNS_OPTION_CODE_SIZE + EDNS_OPTION_LENGTH_SIZE), std::string(ecsOption->second.values.at(0).content, ecsOption->second.values.at(0).size)); } -static void validateResponse(const PacketBuffer& packet, bool hasEdns, uint8_t additionalCount=0) +static void validateResponse(const PacketBuffer& packet, bool hasEdns, uint8_t additionalCount = 0, uint8_t answerCount = 1U) { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) MOADNSParser mdp(false, reinterpret_cast(packet.data()), packet.size()); BOOST_CHECK_EQUAL(mdp.d_qname.toString(), "www.powerdns.com."); BOOST_CHECK_EQUAL(mdp.d_header.qr, 1U); BOOST_CHECK_EQUAL(mdp.d_header.qdcount, 1U); - BOOST_CHECK_EQUAL(mdp.d_header.ancount, 1U); + BOOST_CHECK_EQUAL(mdp.d_header.ancount, answerCount); BOOST_CHECK_EQUAL(mdp.d_header.nscount, 0U); BOOST_CHECK_EQUAL(mdp.d_header.arcount, (hasEdns ? 1U : 0U) + additionalCount); } +static void addOptWithWrongOwnerName(GenericDNSPacketWriter& packetWriter, const GenericDNSPacketWriter::optvect_t& opts) +{ + // same as packetWriter.addOpt(512, 0, 0, opts) except with the wrong owner name + const DNSName notRoot{"dnsdist.org."}; + uint32_t ttl{0}; + EDNS0Record stuff{}; + stuff.version = 0U; + stuff.extFlags = htons(0U); + stuff.extRCode = 0U; + memcpy(&ttl, &stuff, sizeof(stuff)); + ttl = ntohl(ttl); // will be reversed later on + packetWriter.startRecord(notRoot, QType::OPT, ttl, 512U, DNSResourceRecord::ADDITIONAL, false); + for (const auto& option : opts) { + packetWriter.xfr16BitInt(option.first); + packetWriter.xfr16BitInt(option.second.length()); + packetWriter.xfrBlob(option.second); + } + + packetWriter.commit(); +} + BOOST_AUTO_TEST_CASE(test_addXPF) { static const uint16_t xpfOptionCode = 65422; @@ -1100,6 +1122,24 @@ validateResponse(newResponse, false, 1); } +BOOST_AUTO_TEST_CASE(removeEDNSWrongOwnerName) +{ + DNSName name("www.powerdns.com."); + + PacketBuffer response; + GenericDNSPacketWriter packetWriter(response, name, QType::A, QClass::IN, 0); + packetWriter.getHeader()->qr = 1; + addOptWithWrongOwnerName(packetWriter, {}); + + PacketBuffer newResponse; + int res = rewriteResponseWithoutEDNS(response, newResponse); + + BOOST_CHECK_EQUAL(res, 0); + BOOST_CHECK_EQUAL(newResponse.size(), response.size()); + + validateResponse(newResponse, true, 0U, 0U); +} + BOOST_AUTO_TEST_CASE(removeECSWhenOnlyOption) { DNSName name("www.powerdns.com."); ComboAddress origRemote("127.0.0.1"); @@ -1460,6 +1500,32 @@ validateResponse(newResponse, true, 1); } + +BOOST_AUTO_TEST_CASE(rewritingOPTWrongOwnerName) +{ + DNSName name("www.powerdns.com."); + ComboAddress origRemote("127.0.0.1"); + PacketBuffer response; + GenericDNSPacketWriter packetWriter(response, name, QType::A, QClass::IN, 0); + packetWriter.getHeader()->qr = 1; + + EDNSSubnetOpts ecsOpts; + ecsOpts.source = Netmask(origRemote, ECSSourcePrefixV4); + auto origECSOptionStr = makeEDNSSubnetOptsString(ecsOpts); + GenericDNSPacketWriter::optvect_t opts; + opts.emplace_back(EDNSOptionCode::ECS, origECSOptionStr); + + // same as packetWriter.addOpt(512, 0, 0, opts) except with the wrong owner name + addOptWithWrongOwnerName(packetWriter, opts); + + PacketBuffer newResponse; + int res = rewriteResponseWithoutEDNSOption(response, EDNSOptionCode::ECS, newResponse); + BOOST_CHECK_EQUAL(res, 0); + // we should not have modified the payload + BOOST_CHECK_EQUAL(newResponse.size(), response.size()); + validateResponse(newResponse, true, 0U, 0U); +} + static DNSQuestion turnIntoResponse(InternalQueryState& ids, PacketBuffer& query, bool resizeBuffer=true) { if (resizeBuffer) { @@ -2273,4 +2339,37 @@ BOOST_CHECK_EQUAL(cookiesOptionStr, std::string(ecsOption->second.values.at(0).content, ecsOption->second.values.at(0).size)); } +BOOST_AUTO_TEST_CASE(slowRewriteEDNSOptionInQueryWithRecordsWrongOwnerName) +{ + DNSName name("www.powerdns.com."); + + PacketBuffer response; + GenericDNSPacketWriter packetWriter(response, name, QType::A, QClass::IN, 0); + packetWriter.getHeader()->qr = 1; + addOptWithWrongOwnerName(packetWriter, {}); + + uint16_t optStart = 0; + size_t optLen = 0; + bool last = false; + + auto located = locateEDNSOptRR(response, &optStart, &optLen, &last); + BOOST_CHECK_EQUAL(located, ENOENT); + + PacketBuffer newResponse; + bool ednsAdded = false; + bool optionAdded = false; + std::string newEmptyContent; + newEmptyContent.resize(EDNS_OPTION_CODE_SIZE + EDNS_OPTION_LENGTH_SIZE + 1); + auto got = slowRewriteEDNSOptionInQueryWithRecords(response, newResponse, ednsAdded, EDNSOptionCode::ECS, optionAdded, true, newEmptyContent); + BOOST_CHECK_EQUAL(got, true); + BOOST_CHECK_EQUAL(ednsAdded, true); + BOOST_CHECK_EQUAL(optionAdded, true); + BOOST_CHECK_GT(newResponse.size(), response.size()); + + validateResponse(newResponse, true, 1U, 0U); + + located = locateEDNSOptRR(newResponse, &optStart, &optLen, &last); + BOOST_CHECK_EQUAL(located, 0); +} + BOOST_AUTO_TEST_SUITE_END();