From 07b4a19a619a9a8f6ac0f7eddcfcc8745f233092 Mon Sep 17 00:00:00 2001 From: Miod Vallat Date: Wed, 20 May 2026 09:26:22 +0200 Subject: [PATCH 01/13] Make s_last_expired atomic; multiple threads may access it. Signed-off-by: Miod Vallat --- pdns/gss_context.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pdns/gss_context.cc b/pdns/gss_context.cc index 8a9b985d6204..7201b76d574f 100644 --- a/pdns/gss_context.cc +++ b/pdns/gss_context.cc @@ -191,7 +191,7 @@ static void doExpire(T& m, time_t now) static void expire() { - static time_t s_last_expired; + static std::atomic s_last_expired; time_t now = time(nullptr); if (now - s_last_expired < TSIG_GSS_EXPIRE_INTERVAL) { return; From 75a6c915c19796db9599c4d96d184b4eacc5846b Mon Sep 17 00:00:00 2001 From: Miod Vallat Date: Wed, 20 May 2026 09:26:34 +0200 Subject: [PATCH 02/13] Remove redundant GssContext object creation. Signed-off-by: Miod Vallat --- pdns/dnssecinfra.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/pdns/dnssecinfra.cc b/pdns/dnssecinfra.cc index fc0758d7c3b1..ceff465ce919 100644 --- a/pdns/dnssecinfra.cc +++ b/pdns/dnssecinfra.cc @@ -871,7 +871,6 @@ bool validateTSIG(Logr::log_t slog, const std::string& packet, size_t sigPos, co tsigMsg = makeTSIGMessageFromTSIGPacket(packet, sigPos, tt.name, trc, previousMAC, timersOnly, dnsHeaderOffset); if (algo == TSIG_GSS) { - GssContext gssctx(tt.name); if (!gss_verify_signature(slog, tt.name, tsigMsg, theirMAC)) { throw std::runtime_error("Signature with TSIG key '"+tt.name.toLogString()+"' failed to validate"); } From 0b48b39fa1572dd4c6c11cbe07e507008307efd0 Mon Sep 17 00:00:00 2001 From: Miod Vallat Date: Wed, 20 May 2026 09:26:52 +0200 Subject: [PATCH 03/13] Lock around GssSecContext operation. Two distributor threads may create GssContext with the same DNSName label, and thus end up sharing the same GssSecContext internally. Wrapping GssSecContext in LockGuarded makes sure that no concurrent operation can occur. Signed-off-by: Miod Vallat --- pdns/gss_context.cc | 180 ++++++++++++++++++++++++++++---------------- pdns/gss_context.hh | 3 +- 2 files changed, 119 insertions(+), 64 deletions(-) diff --git a/pdns/gss_context.cc b/pdns/gss_context.cc index 7201b76d574f..acefe996813c 100644 --- a/pdns/gss_context.cc +++ b/pdns/gss_context.cc @@ -136,7 +136,7 @@ class GssCredential : boost::noncopyable static LockGuarded>> s_gss_accept_creds; static LockGuarded>> s_gss_init_creds; -class GssSecContext : boost::noncopyable +class GssSecContext { public: GssSecContext(std::shared_ptr cred) @@ -173,7 +173,7 @@ class GssSecContext : boost::noncopyable } d_state{GssStateInitial}; }; // GssSecContext -static LockGuarded>> s_gss_sec_context; +static LockGuarded>>> s_gss_sec_context; template static void doExpire(T& m, time_t now) @@ -189,6 +189,26 @@ static void doExpire(T& m, time_t now) } } +// Same as above, for s_gss_sec_context +template +static void doExpireL(T& m, time_t now) +{ + auto lock = m.lock(); + for (auto i = lock->begin(); i != lock->end();) { + time_t expiretime{0}; + { + auto ctx = i->second->lock(); + expiretime = ctx->d_expires; + } + if (now > expiretime) { + i = lock->erase(i); + } + else { + ++i; + } + } +} + static void expire() { static std::atomic s_last_expired; @@ -199,7 +219,7 @@ static void expire() s_last_expired = now; doExpire(s_gss_init_creds, now); doExpire(s_gss_accept_creds, now); - doExpire(s_gss_sec_context, now); + doExpireL(s_gss_sec_context, now); } bool GssContext::supported() { return true; } @@ -238,18 +258,27 @@ void GssContext::setLabel(const DNSName& label) auto it = lock->find(d_label); if (it != lock->end()) { d_secctx = it->second; - d_type = d_secctx->d_type; + auto ctx = d_secctx->lock(); + d_type = ctx->d_type; } } bool GssContext::expired() { - return (!d_secctx || (d_secctx->d_expires > -1 && d_secctx->d_expires < time(nullptr))); + if (!d_secctx) { + return true; + } + auto ctx = d_secctx->lock(); + return (ctx->d_expires > -1 && ctx->d_expires < time(nullptr)); } bool GssContext::valid() { - return (d_secctx && !expired() && d_secctx->d_state == GssSecContext::GssStateComplete); + if (expired()) { + return false; + } + auto ctx = d_secctx->lock(); + return ctx->d_state == GssSecContext::GssStateComplete; } bool GssContext::init(const std::string& input, std::string& output) @@ -280,7 +309,8 @@ bool GssContext::init(const std::string& input, std::string& output) // see if we can find a context in non-completed state if (d_secctx) { - if (d_secctx->d_state != GssSecContext::GssStateNegotiate) { + auto ctx = d_secctx->lock(); + if (ctx->d_state != GssSecContext::GssStateNegotiate) { d_error = GSS_CONTEXT_INVALID; return false; } @@ -288,43 +318,50 @@ bool GssContext::init(const std::string& input, std::string& output) else { // make context auto lock = s_gss_sec_context.lock(); - d_secctx = std::make_shared(cred); - d_secctx->d_state = GssSecContext::GssStateNegotiate; - d_secctx->d_type = d_type; + d_secctx = std::make_shared>(cred); + { + auto ctx = d_secctx->lock(); + ctx->d_state = GssSecContext::GssStateNegotiate; + ctx->d_type = d_type; + } (*lock)[d_label] = d_secctx; } recv_tok.length = input.size(); recv_tok.value = const_cast(static_cast(input.c_str())); - if (!d_peerPrincipal.empty()) { - buffer.value = const_cast(static_cast(d_peerPrincipal.c_str())); - buffer.length = d_peerPrincipal.size(); - maj = gss_import_name(&min, &buffer, (gss_OID)GSS_KRB5_NT_PRINCIPAL_NAME, &(d_secctx->d_peer_name)); - if (maj != GSS_S_COMPLETE) { - processError("gss_import_name", maj, min); - return false; + { + auto ctx = d_secctx->lock(); + + if (!d_peerPrincipal.empty()) { + buffer.value = const_cast(static_cast(d_peerPrincipal.c_str())); + buffer.length = d_peerPrincipal.size(); + maj = gss_import_name(&min, &buffer, (gss_OID)GSS_KRB5_NT_PRINCIPAL_NAME, &(ctx->d_peer_name)); + if (maj != GSS_S_COMPLETE) { + processError("gss_import_name", maj, min); + return false; + } } - } - maj = gss_init_sec_context(&min, cred->d_cred, &d_secctx->d_ctx, d_secctx->d_peer_name, GSS_C_NO_OID, GSS_C_MUTUAL_FLAG | GSS_C_REPLAY_FLAG, GSS_C_INDEFINITE, GSS_C_NO_CHANNEL_BINDINGS, &recv_tok, nullptr, &send_tok, &flags, &expires); + maj = gss_init_sec_context(&min, cred->d_cred, &ctx->d_ctx, ctx->d_peer_name, GSS_C_NO_OID, GSS_C_MUTUAL_FLAG | GSS_C_REPLAY_FLAG, GSS_C_INDEFINITE, GSS_C_NO_CHANNEL_BINDINGS, &recv_tok, nullptr, &send_tok, &flags, &expires); - if (send_tok.length > 0) { - output.assign(static_cast(send_tok.value), send_tok.length); - tmp_maj = gss_release_buffer(&tmp_min, &send_tok); - } + if (send_tok.length > 0) { + output.assign(static_cast(send_tok.value), send_tok.length); + tmp_maj = gss_release_buffer(&tmp_min, &send_tok); + } - if (maj == GSS_S_COMPLETE) { - // We do not want forever - if (expires == GSS_C_INDEFINITE) { - expires = 60; + if (maj == GSS_S_COMPLETE) { + // We do not want forever + if (expires == GSS_C_INDEFINITE) { + expires = 60; + } + ctx->d_expires = time(nullptr) + expires; + ctx->d_state = GssSecContext::GssStateComplete; + return true; + } + else if (maj != GSS_S_CONTINUE_NEEDED) { + processError("gss_init_sec_context", maj, min); } - d_secctx->d_expires = time(nullptr) + expires; - d_secctx->d_state = GssSecContext::GssStateComplete; - return true; - } - else if (maj != GSS_S_CONTINUE_NEEDED) { - processError("gss_init_sec_context", maj, min); } return (maj == GSS_S_CONTINUE_NEEDED); @@ -358,7 +395,8 @@ bool GssContext::accept(const std::string& input, std::string& output) // see if we can find a context in non-completed state if (d_secctx) { - if (d_secctx->d_state != GssSecContext::GssStateNegotiate) { + auto ctx = d_secctx->lock(); + if (ctx->d_state != GssSecContext::GssStateNegotiate) { d_error = GSS_CONTEXT_INVALID; return false; } @@ -366,34 +404,41 @@ bool GssContext::accept(const std::string& input, std::string& output) else { // make context auto lock = s_gss_sec_context.lock(); - d_secctx = std::make_shared(cred); - d_secctx->d_state = GssSecContext::GssStateNegotiate; - d_secctx->d_type = d_type; + d_secctx = std::make_shared>(cred); + { + auto ctx = d_secctx->lock(); + ctx->d_state = GssSecContext::GssStateNegotiate; + ctx->d_type = d_type; + } (*lock)[d_label] = d_secctx; } recv_tok.length = input.size(); recv_tok.value = const_cast(static_cast(input.c_str())); - maj = gss_accept_sec_context(&min, &d_secctx->d_ctx, cred->d_cred, &recv_tok, GSS_C_NO_CHANNEL_BINDINGS, &d_secctx->d_peer_name, nullptr, &send_tok, &flags, &expires, nullptr); + { + auto ctx = d_secctx->lock(); + maj = gss_accept_sec_context(&min, &ctx->d_ctx, cred->d_cred, &recv_tok, GSS_C_NO_CHANNEL_BINDINGS, &ctx->d_peer_name, nullptr, &send_tok, &flags, &expires, nullptr); - if (send_tok.length > 0) { - output.assign(static_cast(send_tok.value), send_tok.length); - tmp_maj = gss_release_buffer(&tmp_min, &send_tok); - } + if (send_tok.length > 0) { + output.assign(static_cast(send_tok.value), send_tok.length); + tmp_maj = gss_release_buffer(&tmp_min, &send_tok); + } - if (maj == GSS_S_COMPLETE) { - // We do not want forever - if (expires == GSS_C_INDEFINITE) { - expires = 60; + if (maj == GSS_S_COMPLETE) { + // We do not want forever + if (expires == GSS_C_INDEFINITE) { + expires = 60; + } + ctx->d_expires = time(nullptr) + expires; + ctx->d_state = GssSecContext::GssStateComplete; + return true; + } + else if (maj != GSS_S_CONTINUE_NEEDED) { + processError("gss_accept_sec_context", maj, min); } - d_secctx->d_expires = time(nullptr) + expires; - d_secctx->d_state = GssSecContext::GssStateComplete; - return true; - } - else if (maj != GSS_S_CONTINUE_NEEDED) { - processError("gss_accept_sec_context", maj, min); } + return (maj == GSS_S_CONTINUE_NEEDED); }; @@ -408,7 +453,10 @@ bool GssContext::sign(const std::string& input, std::string& output) recv_tok.length = input.size(); recv_tok.value = const_cast(static_cast(input.c_str())); - maj = gss_get_mic(&min, d_secctx->d_ctx, GSS_C_QOP_DEFAULT, &recv_tok, &send_tok); + { + auto ctx = d_secctx->lock(); + maj = gss_get_mic(&min, ctx->d_ctx, GSS_C_QOP_DEFAULT, &recv_tok, &send_tok); + } if (send_tok.length > 0) { output.assign(static_cast(send_tok.value), send_tok.length); @@ -434,7 +482,10 @@ bool GssContext::verify(const std::string& input, const std::string& signature) sign_tok.length = signature.size(); sign_tok.value = const_cast(static_cast(signature.c_str())); - maj = gss_verify_mic(&min, d_secctx->d_ctx, &recv_tok, &sign_tok, nullptr); + { + auto ctx = d_secctx->lock(); + maj = gss_verify_mic(&min, ctx->d_ctx, &recv_tok, &sign_tok, nullptr); + } if (maj != GSS_S_COMPLETE) { processError("gss_get_mic", maj, min); @@ -473,20 +524,23 @@ bool GssContext::getPeerPrincipal(std::string& name) gss_buffer_desc value; OM_uint32 maj, min; - if (d_secctx->d_peer_name != GSS_C_NO_NAME) { - maj = gss_display_name(&min, d_secctx->d_peer_name, &value, nullptr); - if (maj == GSS_S_COMPLETE && value.length > 0) { - name.assign(static_cast(value.value), value.length); - maj = gss_release_buffer(&min, &value); - return true; + { + auto ctx = d_secctx->lock(); + if (ctx->d_peer_name != GSS_C_NO_NAME) { + maj = gss_display_name(&min, ctx->d_peer_name, &value, nullptr); + if (maj == GSS_S_COMPLETE && value.length > 0) { + name.assign(static_cast(value.value), value.length); + maj = gss_release_buffer(&min, &value); + return true; + } + else { + return false; + } } else { return false; } } - else { - return false; - } } std::tuple GssContext::getCounts() diff --git a/pdns/gss_context.hh b/pdns/gss_context.hh index e19873674f5d..0571f0d54ac0 100644 --- a/pdns/gss_context.hh +++ b/pdns/gss_context.hh @@ -28,6 +28,7 @@ #include "namespaces.hh" #include "pdnsexception.hh" #include "dns.hh" +#include "lock.hh" #include "logr.hh" #ifdef ENABLE_GSS_TSIG @@ -206,7 +207,7 @@ private: GssContextError d_error; // d_gss_errors; // d_secctx; //> d_secctx; // Date: Wed, 20 May 2026 09:27:03 +0200 Subject: [PATCH 04/13] Factor code responsible for GssSecContext acquisition. Signed-off-by: Miod Vallat --- pdns/gss_context.cc | 64 ++++++++++++++++++++------------------------- pdns/gss_context.hh | 2 ++ 2 files changed, 30 insertions(+), 36 deletions(-) diff --git a/pdns/gss_context.cc b/pdns/gss_context.cc index acefe996813c..6c7144587585 100644 --- a/pdns/gss_context.cc +++ b/pdns/gss_context.cc @@ -281,6 +281,30 @@ bool GssContext::valid() return ctx->d_state == GssSecContext::GssStateComplete; } +bool GssContext::createOrReuseContext(std::shared_ptr cred) +{ + // see if we can find a context in non-completed state + if (d_secctx) { + auto ctx = d_secctx->lock(); + if (ctx->d_state != GssSecContext::GssStateNegotiate) { + d_error = GSS_CONTEXT_INVALID; + return false; + } + } + else { + // make context + auto lock = s_gss_sec_context.lock(); + d_secctx = std::make_shared>(cred); + { + auto ctx = d_secctx->lock(); + ctx->d_state = GssSecContext::GssStateNegotiate; + ctx->d_type = d_type; + } + (*lock)[d_label] = d_secctx; + } + return true; +} + bool GssContext::init(const std::string& input, std::string& output) { expire(); @@ -307,24 +331,8 @@ bool GssContext::init(const std::string& input, std::string& output) cred = it->second; } - // see if we can find a context in non-completed state - if (d_secctx) { - auto ctx = d_secctx->lock(); - if (ctx->d_state != GssSecContext::GssStateNegotiate) { - d_error = GSS_CONTEXT_INVALID; - return false; - } - } - else { - // make context - auto lock = s_gss_sec_context.lock(); - d_secctx = std::make_shared>(cred); - { - auto ctx = d_secctx->lock(); - ctx->d_state = GssSecContext::GssStateNegotiate; - ctx->d_type = d_type; - } - (*lock)[d_label] = d_secctx; + if (!createOrReuseContext(cred)) { + return false; } recv_tok.length = input.size(); @@ -393,24 +401,8 @@ bool GssContext::accept(const std::string& input, std::string& output) cred = it->second; } - // see if we can find a context in non-completed state - if (d_secctx) { - auto ctx = d_secctx->lock(); - if (ctx->d_state != GssSecContext::GssStateNegotiate) { - d_error = GSS_CONTEXT_INVALID; - return false; - } - } - else { - // make context - auto lock = s_gss_sec_context.lock(); - d_secctx = std::make_shared>(cred); - { - auto ctx = d_secctx->lock(); - ctx->d_state = GssSecContext::GssStateNegotiate; - ctx->d_type = d_type; - } - (*lock)[d_label] = d_secctx; + if (!createOrReuseContext(cred)) { + return false; } recv_tok.length = input.size(); diff --git a/pdns/gss_context.hh b/pdns/gss_context.hh index 0571f0d54ac0..fc37ff7820b4 100644 --- a/pdns/gss_context.hh +++ b/pdns/gss_context.hh @@ -58,6 +58,7 @@ enum GssContextType }; class GssSecContext; +class GssCredential; /*! Class for representing GSS names, such as host/host.domain.com@REALM. */ @@ -200,6 +201,7 @@ private: void initialize(); // cred); #endif DNSName d_label; // Date: Wed, 20 May 2026 09:27:35 +0200 Subject: [PATCH 05/13] Add a configurable limit to the number of active GSS contexts. Signed-off-by: Miod Vallat --- pdns/auth-main.cc | 4 ++++ pdns/gss_context.cc | 7 +++++++ pdns/gss_context.hh | 6 +++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/pdns/auth-main.cc b/pdns/auth-main.cc index cb684a63ff1b..f3a6645bcad7 100644 --- a/pdns/auth-main.cc +++ b/pdns/auth-main.cc @@ -353,6 +353,7 @@ static void declareArguments() #ifdef ENABLE_GSS_TSIG ::arg().setSwitch("enable-gss-tsig", "Enable GSS TSIG processing") = "no"; + ::arg().set("gss-max-contexts", "The maximum number of simultaneous GSS contexts allowed") = "1000"; #endif ::arg().setSwitch("views", "Enable views (variants) of zones, for backends which support them") = "no"; @@ -786,6 +787,9 @@ static void mainthread() #endif #ifdef ENABLE_GSS_TSIG g_doGssTSIG = ::arg().mustDo("enable-gss-tsig"); + if (g_doGssTSIG) { + GssContext::s_maxGssContexts = ::arg().asNum("gss-max-contexts"); + } #endif g_views = ::arg().mustDo("views"); g_memberCatalogGroup = ::arg()["member-catalog-group"]; diff --git a/pdns/gss_context.cc b/pdns/gss_context.cc index 6c7144587585..c1945b27246f 100644 --- a/pdns/gss_context.cc +++ b/pdns/gss_context.cc @@ -55,6 +55,8 @@ GssContextError GssContext::getError() { return GSS_CONTEXT_UNSUPPORTED; } #define TSIG_GSS_EXPIRE_INTERVAL 60 +unsigned int GssContext::s_maxGssContexts{1000}; + class GssCredential : boost::noncopyable { public: @@ -294,6 +296,11 @@ bool GssContext::createOrReuseContext(std::shared_ptr cred) else { // make context auto lock = s_gss_sec_context.lock(); + if (lock->size() == s_maxGssContexts) { + d_error = GSS_CONTEXT_LIMIT_REACHED; + d_gss_errors.push_back("Limit of concurrent GSS contexts reached"); + return false; + } d_secctx = std::make_shared>(cred); { auto ctx = d_secctx->lock(); diff --git a/pdns/gss_context.hh b/pdns/gss_context.hh index fc37ff7820b4..e2f4f28815cb 100644 --- a/pdns/gss_context.hh +++ b/pdns/gss_context.hh @@ -46,7 +46,8 @@ enum GssContextError GSS_CONTEXT_NOT_INITIALIZED, GSS_CONTEXT_INVALID, GSS_CONTEXT_EXPIRED, - GSS_CONTEXT_ALREADY_INITIALIZED + GSS_CONTEXT_ALREADY_INITIALIZED, + GSS_CONTEXT_LIMIT_REACHED, }; //! GSS context types @@ -196,6 +197,9 @@ public: GssContextError getError(); // getErrorStrings() { return d_gss_errors; } // Date: Wed, 20 May 2026 09:28:22 +0200 Subject: [PATCH 06/13] Cope with exceptions thrown by MOADNSParser initialization. Signed-off-by: Miod Vallat --- pdns/axfr-retriever.cc | 58 ++++++++++---------- pdns/comfun.cc | 7 ++- pdns/dnspacket.cc | 119 +++++++++++++++++++++++++---------------- pdns/resolver.cc | 75 +++++++++++++++----------- pdns/stubresolver.cc | 34 +++++++----- 5 files changed, 170 insertions(+), 123 deletions(-) diff --git a/pdns/axfr-retriever.cc b/pdns/axfr-retriever.cc index b11895a39801..35193c746c33 100644 --- a/pdns/axfr-retriever.cc +++ b/pdns/axfr-retriever.cc @@ -128,48 +128,46 @@ int AXFRRetriever::getChunk(Resolver::res_t &res, vector* records, ui d_receivedBytes += (uint16_t) len; - MOADNSParser mdp(false, d_buf.data(), len); - - int err = mdp.d_header.rcode; + try { + MOADNSParser mdp(false, d_buf.data(), len); - if(err) { - throw ResolverException("AXFR chunk error: " + RCode::to_s(err)); - } + int err = mdp.d_header.rcode; + if (err != 0) { + throw ResolverException("AXFR chunk error: " + RCode::to_s(err)); + } - if(mdp.d_header.tc) { - throw ResolverException("AXFR chunk had TC bit set"); - } + if(mdp.d_header.tc) { + throw ResolverException("AXFR chunk had TC bit set"); + } - try { d_tsigVerifier.check(std::string(d_buf.data(), len), mdp); - } - catch(const std::runtime_error& re) { - throw ResolverException(re.what()); - } - if(!records) { - err = parseResult(mdp, DNSName(), 0, 0, &res); - - if (!err) { - for(const auto& answer : mdp.d_answers) { - if (answer.d_type == QType::SOA) { - d_soacount++; + if (records == nullptr) { + err = parseResult(mdp, DNSName(), 0, 0, &res); + if (err == 0) { + for(const auto& answer : mdp.d_answers) { + if (answer.d_type == QType::SOA) { + d_soacount++; + } } } } - } - else { - records->clear(); - records->reserve(mdp.d_answers.size()); + else { + records->clear(); + records->reserve(mdp.d_answers.size()); - for(auto& r: mdp.d_answers) { - if (r.d_type == QType::SOA) { - d_soacount++; - } + for(auto& r: mdp.d_answers) { + if (r.d_type == QType::SOA) { + d_soacount++; + } - records->push_back(std::move(r)); + records->push_back(std::move(r)); + } } } + catch(const std::runtime_error& re) { + throw ResolverException(re.what()); + } return true; } diff --git a/pdns/comfun.cc b/pdns/comfun.cc index 3aafd8142b25..9e7af8932d88 100644 --- a/pdns/comfun.cc +++ b/pdns/comfun.cc @@ -550,6 +550,9 @@ try } // cout<topAllocatorsString(20)<(answer); - if (!content) { - SLOG(g_log<info(Logr::Error, "TSIG record has no or invalid content (invalid packet)")); - return false; + bool gotit=false; + for(const auto & answer : mdp.d_answers) { + if(answer.d_type == QType::TSIG && answer.d_class == QType::ANY) { + // cast can fail, f.e. if d_content is an UnknownRecordContent. + auto content = getRR(answer); + if (!content) { + SLOG(g_log<info(Logr::Error, "TSIG record has no or invalid content (invalid packet)")); + return false; + } + *trc = *content; + *keyname = answer.d_name; + gotit=true; } - *trc = *content; - *keyname = answer.d_name; - gotit=true; } + if(!gotit) + return false; + + if (tsigPosOut != nullptr) { + *tsigPosOut = tsigPos; + } + + return true; } - if(!gotit) + catch (const MOADNSException&) { + // If execution has reached this routine, we can reasonably assume that + // the packet is good enough to pass the sanity checks of + // MOADNSParser::init(). But just in case it doesn't, better handle this. return false; - - if (tsigPosOut) { - *tsigPosOut = tsigPos; } - - return true; } bool DNSPacket::validateTSIG(const TSIGTriplet& tsigTriplet, const TSIGRecordContent& tsigContent, const std::string& previousMAC, const std::string& theirMAC, bool timersOnly) const { - MOADNSParser mdp(d_isQuery, d_rawpacket); - uint16_t tsigPos = mdp.getTSIGPos(); - if (tsigPos == 0) { + try { + MOADNSParser mdp(d_isQuery, d_rawpacket); + uint16_t tsigPos = mdp.getTSIGPos(); + if (tsigPos == 0) { + return false; + } + + return ::validateTSIG(d_slog, d_rawpacket, tsigPos, tsigTriplet, tsigContent, previousMAC, theirMAC, timersOnly); + } + catch (const MOADNSException&) { + // If execution has reached this routine, we can reasonably assume that + // the packet is good enough to pass the sanity checks of + // MOADNSParser::init(). But just in case it doesn't, better handle this. return false; } - - return ::validateTSIG(d_slog, d_rawpacket, tsigPos, tsigTriplet, tsigContent, previousMAC, theirMAC, timersOnly); } bool DNSPacket::getTKEYRecord(TKEYRecordContent *tr, DNSName *keyname) const { - MOADNSParser mdp(d_isQuery, d_rawpacket); - bool gotit=false; - - for(const auto & answer : mdp.d_answers) { - if (gotit) { - SLOG(g_log<info(Logr::Error, "More than one TKEY record found in query")); - return false; - } + try { + MOADNSParser mdp(d_isQuery, d_rawpacket); + bool gotit=false; - if(answer.d_type == QType::TKEY) { - // cast can fail, f.e. if d_content is an UnknownRecordContent. - auto content = getRR(answer); - if (!content) { - SLOG(g_log<info(Logr::Error, "TKEY record has no or invalid content (invalid packet)")); + for(const auto & answer : mdp.d_answers) { + if (gotit) { + SLOG(g_log<info(Logr::Error, "More than one TKEY record found in query")); return false; } - *tr = *content; - *keyname = answer.d_name; - gotit=true; + + if(answer.d_type == QType::TKEY) { + // cast can fail, f.e. if d_content is an UnknownRecordContent. + auto content = getRR(answer); + if (!content) { + SLOG(g_log<info(Logr::Error, "TKEY record has no or invalid content (invalid packet)")); + return false; + } + *tr = *content; + *keyname = answer.d_name; + gotit=true; + } } - } - return gotit; + return gotit; + } + catch (const MOADNSException&) { + // If execution has reached this routine, we can reasonably assume that + // the packet is good enough to pass the sanity checks of + // MOADNSParser::init(). But just in case it doesn't, better handle this. + return false; + } } /** This function takes data from the network, possibly received with recvfrom, and parses diff --git a/pdns/resolver.cc b/pdns/resolver.cc index adbb469d0abf..05c0446acfba 100644 --- a/pdns/resolver.cc +++ b/pdns/resolver.cc @@ -269,42 +269,52 @@ bool Resolver::tryGetSOASerial(DNSName *domain, ComboAddress* remote, uint32_t * throw ResolverException("recvfrom error waiting for answer: "+stringerror()); } - MOADNSParser mdp(false, (char*)buf, err); - *id=mdp.d_header.id; - *domain = mdp.d_qname; - - if(domain->empty()) - throw ResolverException("SOA query to '" + remote->toLogString() + "' produced response without domain name (RCode: " + RCode::to_s(mdp.d_header.rcode) + ")"); - - if(mdp.d_answers.empty()) - throw ResolverException("Query to '" + remote->toLogString() + "' for SOA of '" + domain->toLogString() + "' produced no results (RCode: " + RCode::to_s(mdp.d_header.rcode) + ")"); - - if(mdp.d_qtype != QType::SOA) - throw ResolverException("Query to '" + remote->toLogString() + "' for SOA of '" + domain->toLogString() + "' returned wrong record type"); - - if(mdp.d_header.rcode != 0) - throw ResolverException("Query to '" + remote->toLogString() + "' for SOA of '" + domain->toLogString() + "' returned Rcode " + RCode::to_s(mdp.d_header.rcode)); - - *theirInception = *theirExpire = 0; - bool gotSOA=false; - for(const MOADNSParser::answers_t::value_type& drc : mdp.d_answers) { - if(drc.d_type == QType::SOA && drc.d_name == *domain) { - auto src = getRR(drc); - if (src) { - *theirSerial = src->d_st.serial; - gotSOA = true; - } + bool gotSOA{false}; + try { + MOADNSParser mdp(false, (char*)buf, err); + *id=mdp.d_header.id; + *domain = mdp.d_qname; + + if(domain->empty()) { + throw ResolverException("SOA query to '" + remote->toLogString() + "' produced response without domain name (RCode: " + RCode::to_s(mdp.d_header.rcode) + ")"); + } + + if(mdp.d_answers.empty()) { + throw ResolverException("Query to '" + remote->toLogString() + "' for SOA of '" + domain->toLogString() + "' produced no results (RCode: " + RCode::to_s(mdp.d_header.rcode) + ")"); + } + + if(mdp.d_qtype != QType::SOA) { + throw ResolverException("Query to '" + remote->toLogString() + "' for SOA of '" + domain->toLogString() + "' returned wrong record type"); } - if(drc.d_type == QType::RRSIG && drc.d_name == *domain) { - auto rrc = getRR(drc); - if(rrc && rrc->d_type == QType::SOA) { - *theirInception= std::max(*theirInception, rrc->d_siginception); - *theirExpire = std::max(*theirExpire, rrc->d_sigexpire); + + if(mdp.d_header.rcode != 0) { + throw ResolverException("Query to '" + remote->toLogString() + "' for SOA of '" + domain->toLogString() + "' returned Rcode " + RCode::to_s(mdp.d_header.rcode)); + } + + *theirInception = *theirExpire = 0; + for(const MOADNSParser::answers_t::value_type& drc : mdp.d_answers) { + if(drc.d_type == QType::SOA && drc.d_name == *domain) { + auto src = getRR(drc); + if (src) { + *theirSerial = src->d_st.serial; + gotSOA = true; + } + } + if(drc.d_type == QType::RRSIG && drc.d_name == *domain) { + auto rrc = getRR(drc); + if(rrc && rrc->d_type == QType::SOA) { + *theirInception= std::max(*theirInception, rrc->d_siginception); + *theirExpire = std::max(*theirExpire, rrc->d_sigexpire); + } } } } - if(!gotSOA) + catch (const MOADNSException& exc) { + throw ResolverException("SOA Query to '" + remote->toLogString() + "' produced ill-formed response: " + exc.what()); + } + if(!gotSOA) { throw ResolverException("Query to '" + remote->toLogString() + "' for SOA of '" + domain->toLogString() + "' did not return a SOA"); + } return true; } @@ -339,6 +349,9 @@ int Resolver::resolve(const ComboAddress& to, const DNSName &domain, int type, R catch(ResolverException &re) { throw ResolverException(re.reason+" from "+to.toLogString()); } + catch (const MOADNSException& exc) { + throw ResolverException(std::string(exc.what()) + " from " + to.toLogString()); + } } int Resolver::resolve(const ComboAddress& ipport, const DNSName &domain, int type, Resolver::res_t* res) { diff --git a/pdns/stubresolver.cc b/pdns/stubresolver.cc index a6da7039d121..f095616274b6 100644 --- a/pdns/stubresolver.cc +++ b/pdns/stubresolver.cc @@ -174,22 +174,30 @@ int stubDoResolve(Logr::log_t slog, const DNSName& qname, uint16_t qtype, vector catch (...) { continue; } - MOADNSParser mdp(false, reply); - if (mdp.d_header.rcode == RCode::ServFail) { - continue; - } - for (const auto& answer : mdp.d_answers) { - if (answer.d_place == 1 && answer.d_type == qtype) { - DNSZoneRecord zrr; - zrr.dr = answer; - zrr.auth = true; - ret.push_back(std::move(zrr)); + try { + MOADNSParser mdp(false, reply); + if (mdp.d_header.rcode == RCode::ServFail) { + continue; + } + + for (const auto& answer : mdp.d_answers) { + if (answer.d_place == 1 && answer.d_type == qtype) { + DNSZoneRecord zrr; + zrr.dr = answer; + zrr.auth = true; + ret.push_back(std::move(zrr)); + } } + SLOG(g_log << Logger::Debug << logPrefix << "Question for '" << queryNameType << "' got answered by " << dest.toString() << endl, + slog->info(Logr::Debug, "stub-resolver: got an answer", "query", Logging::Loggable(qname), "type", Logging::Loggable(QType(qtype)), "resolver", Logging::Loggable(dest))); + return mdp.d_header.rcode; + } + catch (const MOADNSException& exc) { + SLOG(g_log << Logger::Debug << logPrefix << "Question for '" << queryNameType << "' got ill-formed answer from " << dest.toString() << ": " << exc.what() << endl, + slog->error(Logr::Debug, exc.what(), "stub-resolver: got an ill-formed answer", "query", Logging::Loggable(qname), "type", Logging::Loggable(QType(qtype)), "resolver", Logging::Loggable(dest))); + continue; } - SLOG(g_log << Logger::Debug << logPrefix << "Question for '" << queryNameType << "' got answered by " << dest.toString() << endl, - slog->info(Logr::Debug, "stub-resolver: got an answer", "query", Logging::Loggable(qname), "type", Logging::Loggable(QType(qtype)), "resolver", Logging::Loggable(dest))); - return mdp.d_header.rcode; } return RCode::ServFail; } From 6d6206e8f245b4d9c324f2441a365906d2a62db5 Mon Sep 17 00:00:00 2001 From: Miod Vallat Date: Wed, 20 May 2026 09:29:52 +0200 Subject: [PATCH 07/13] Escape bind-special characters in rrnames when writing bind zones. Signed-off-by: Miod Vallat --- modules/bindbackend/bindbackend2.cc | 33 +++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/modules/bindbackend/bindbackend2.cc b/modules/bindbackend/bindbackend2.cc index 24fb4e4fad79..b89355b7427a 100644 --- a/modules/bindbackend/bindbackend2.cc +++ b/modules/bindbackend/bindbackend2.cc @@ -337,6 +337,35 @@ static void stripDomainSuffix(string* qname, const ZoneName& zonename) } } +// Perform adequate escaping of characters which have special meaning in +// Bind zone files. +// Note that the input is supposed to be a DNSName::toString() - or any of +// its variants - so we assume \ and . have been correctly escaped by +// DNSName::appendEscapedLabel already. +static const std::string bindEscape(const std::string& name) +{ + std::string ret; + std::array ebuf{}; + + for (char letter : name) { + switch (letter) { + case '$': + case '@': + case '"': + case ';': + case '(': + case ')': + snprintf(ebuf.data(), ebuf.size(), "\\%03u", static_cast(letter)); + ret += ebuf.data(); + break; + default: + ret += letter; + break; + } + } + return ret; +} + bool Bind2Backend::feedRecord(const DNSResourceRecord& rr, const DNSName& /* ordername */, bool /* ordernameIsNSEC3 */) { if (d_transaction_id == UnknownDomainID) { @@ -345,7 +374,7 @@ bool Bind2Backend::feedRecord(const DNSResourceRecord& rr, const DNSName& /* ord string qname; if (d_transaction_qname.empty()) { - qname = rr.qname.toString(); + qname = bindEscape(rr.qname.toString()); } else if (rr.qname.isPartOf(d_transaction_qname)) { if (rr.qname == d_transaction_qname.operator const DNSName&()) { @@ -353,7 +382,7 @@ bool Bind2Backend::feedRecord(const DNSResourceRecord& rr, const DNSName& /* ord } else { DNSName relName = rr.qname.makeRelative(d_transaction_qname); - qname = relName.toStringNoDot(); + qname = bindEscape(relName.toStringNoDot()); } } else { From 498662546cea848ff984c72765946a16c8eb73f7 Mon Sep 17 00:00:00 2001 From: Miod Vallat Date: Wed, 20 May 2026 09:30:23 +0200 Subject: [PATCH 08/13] Use getInnerRemote() instead of inlining it. NFC Signed-off-by: Miod Vallat --- pdns/auth-main.cc | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pdns/auth-main.cc b/pdns/auth-main.cc index f3a6645bcad7..5c9aa7c2a296 100644 --- a/pdns/auth-main.cc +++ b/pdns/auth-main.cc @@ -629,10 +629,7 @@ static void qthread(unsigned int num) numreceived++; - accountremote = question.d_remote; - if (question.d_inner_remote) { - accountremote = *question.d_inner_remote; - } + accountremote = question.getInnerRemote(); if (accountremote.sin4.sin_family == AF_INET) { numreceived4++; From 8d1595a39297a200457163eedd4744e29606ee14 Mon Sep 17 00:00:00 2001 From: Miod Vallat Date: Wed, 20 May 2026 09:30:39 +0200 Subject: [PATCH 09/13] Use the inner remote to perform view selection. Signed-off-by: Miod Vallat --- pdns/tcpreceiver.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pdns/tcpreceiver.cc b/pdns/tcpreceiver.cc index 60b33fb22e87..4b8604d2d6a7 100644 --- a/pdns/tcpreceiver.cc +++ b/pdns/tcpreceiver.cc @@ -408,7 +408,7 @@ void TCPNameserver::doConnection(int fd, Logr::log_t slog) if (packet->couldBeCached()) { std::string view{}; if (g_views) { - Netmask netmask(packet->d_remote); + Netmask netmask(packet->getInnerRemote()); view = g_zoneCache.getViewFromNetwork(&netmask); } if (PC.get(*packet, *cached, view)) { // short circuit - does the PacketCache recognize this question? From 5179659c5e1e6f26af97018fd8bcd8015cec25af Mon Sep 17 00:00:00 2001 From: Miod Vallat Date: Wed, 20 May 2026 09:30:51 +0200 Subject: [PATCH 10/13] Crude test for proxy + views Signed-off-by: Miod Vallat --- .../test_ProxyProtocol.py | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/regression-tests.auth-py/test_ProxyProtocol.py b/regression-tests.auth-py/test_ProxyProtocol.py index f438aec73025..a92d88443226 100644 --- a/regression-tests.auth-py/test_ProxyProtocol.py +++ b/regression-tests.auth-py/test_ProxyProtocol.py @@ -4,6 +4,7 @@ import socket import struct import subprocess +import unittest from authtests import AuthTest from proxyprotocol import ProxyProtocol @@ -267,3 +268,74 @@ def testAXFR(self): res = dns.message.from_wire(data) self.assertRcodeEqual(res, expectedrcode) + + +class TestProxyProtocolViews(AuthTest): + _config_template = """ +launch={backend} +proxy-protocol-from=127.0.0.1 +""" + + @classmethod + def setUpClass(cls): + super(TestProxyProtocolViews, cls).setUpClass() + + if cls._backend == "lmdb": + os.system("$PDNSUTIL --config-dir=configs/auth zone create views.test") + os.system( + "$PDNSUTIL --config-dir=configs/auth rrset replace views.test views.test SOA 'no.where no.where 1 10800 3600 604800 3600'" + ) + os.system("$PDNSUTIL --config-dir=configs/auth zone create views.test..squint") + os.system( + "$PDNSUTIL --config-dir=configs/auth rrset replace views.test..squint views.test SOA 'no.where no.where 1 10800 3600 604800 3600'" + ) + os.system("$PDNSUTIL --config-dir=configs/auth rrset add views.test..squint a.views.test A 1.2.3.4") + os.system("$PDNSUTIL --config-dir=configs/auth network set 192.168.0.0/16 squinted") + os.system("$PDNSUTIL --config-dir=configs/auth view add-zone squinted views.test..squint") + os.system("$PDNSCONTROL --socket-dir=configs/auth rediscover") + + def testViews(self): + """ + Check that views correctly operate on the PROXY header inner address + """ + + if self._backend == "lmdb": + query = dns.message.make_query("a.views.test", "A") + + queryPayload = query.to_wire() + + for task in ( + ("192.0.2.1", dns.rcode.NXDOMAIN), + ("192.168.2.53", dns.rcode.NOERROR), + ): + ip, expectedrcode = task + + ppPayload = ProxyProtocol.getPayload(False, True, False, ip, "10.1.2.3", 12345, 53, []) + + # TCP + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(2.0) + sock.connect(("127.0.0.1", self._authPort)) + + try: + sock.send(ppPayload) + sock.send(struct.pack("!H", len(queryPayload))) + sock.send(queryPayload) + data = sock.recv(2) + if data: + (datalen,) = struct.unpack("!H", data) + data = sock.recv(datalen) + except socket.timeout as e: + print("Timeout: %s" % (str(e))) + data = None + except socket.error as e: + print("Network error: %s" % (str(e))) + data = None + finally: + sock.close() + + res = None + if data: + res = dns.message.from_wire(data) + + self.assertRcodeEqual(res, expectedrcode) From 0021bb24adb7cbfb1c12b11ffb83212c073c9fdf Mon Sep 17 00:00:00 2001 From: Miod Vallat Date: Wed, 20 May 2026 09:31:31 +0200 Subject: [PATCH 11/13] Simplify PTR record creation. NFCI Signed-off-by: Miod Vallat --- pdns/auth-catalogzone.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pdns/auth-catalogzone.cc b/pdns/auth-catalogzone.cc index 828d9f6137a9..f791f80516ce 100644 --- a/pdns/auth-catalogzone.cc +++ b/pdns/auth-catalogzone.cc @@ -147,7 +147,7 @@ void CatalogInfo::toDNSZoneRecords(const ZoneName& zone, vector& DNSZoneRecord dzr; dzr.dr.d_name = prefix; dzr.dr.d_type = QType::PTR; - dzr.dr.setContent(std::make_shared(d_zone.operator const DNSName&().toString())); + dzr.dr.setContent(std::make_shared(d_zone.operator const DNSName&())); dzrs.emplace_back(dzr); // coo property From cfb353beff45cc9e11ab7549b23eb08a3dcb48f4 Mon Sep 17 00:00:00 2001 From: Miod Vallat Date: Wed, 20 May 2026 09:31:40 +0200 Subject: [PATCH 12/13] Be sure to escape user data when building a TXT record. Signed-off-by: Miod Vallat --- pdns/auth-catalogzone.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pdns/auth-catalogzone.cc b/pdns/auth-catalogzone.cc index f791f80516ce..0c9cd10a85a6 100644 --- a/pdns/auth-catalogzone.cc +++ b/pdns/auth-catalogzone.cc @@ -162,7 +162,7 @@ void CatalogInfo::toDNSZoneRecords(const ZoneName& zone, vector& for (const auto& group : d_group) { dzr.dr.d_name = g_groupdnsname + prefix; dzr.dr.d_type = QType::TXT; - dzr.dr.setContent(std::make_shared("\"" + group + "\"")); + dzr.dr.setContent(std::make_shared("\"" + txtEscape(group) + "\"")); dzrs.emplace_back(dzr); } } From 62e40c9bdfd669798cf3524d7eb24dcf5965f916 Mon Sep 17 00:00:00 2001 From: Miod Vallat Date: Wed, 20 May 2026 10:15:30 +0200 Subject: [PATCH 13/13] documentation and secpoll update for auth 4.9.15 and 5.0.5 Signed-off-by: Miod Vallat --- docs/changelog/4.9.rst | 92 ++++++++++++++ docs/changelog/5.0.rst | 99 +++++++++++++++ docs/secpoll.zone | 10 +- .../powerdns-advisory-2026-06.rst | 119 ++++++++++++++++++ 4 files changed, 316 insertions(+), 4 deletions(-) create mode 100644 docs/security-advisories/powerdns-advisory-2026-06.rst diff --git a/docs/changelog/4.9.rst b/docs/changelog/4.9.rst index 716ed9fbe387..95cdb06b4b21 100644 --- a/docs/changelog/4.9.rst +++ b/docs/changelog/4.9.rst @@ -1,6 +1,98 @@ Changelogs for 4.9.x ==================== +.. changelog:: + :version: 4.9.15 + :released: 20th of May 2026 + + This is release 4.9.15 of the Authoritative Server. + It contains bug fixes and security fixes. + + Please review the :doc:`Upgrade Notes <../upgrading>` before upgrading from versions < 4.9.x. + + .. change:: + :tags: Bug Fixes + :pullreq: 17444 + + Fix PowerDNS Security Advisory 2026-06 for PowerDNS Authoritative Server: Multiple Issues + + .. change:: + :tags: Bug Fixes + :pullreq: 17295 + :tickets: 17284 + + use less inefficient code in web server + + .. change:: + :tags: Bug Fixes + :pullreq: 17293 + :tickets: 17240 + + harden xfr*BitInt writers + + .. change:: + :tags: Bug Fixes + :pullreq: 17260 + :tickets: 16636 + + perform axfr immediately when creating an autosecondary domain + + .. change:: + :tags: Bug Fixes + :pullreq: 17262 + :tickets: 16731 + + web: stricter control of statistics rings changes + + .. change:: + :tags: Bug Fixes + :pullreq: 17265 + :tickets: 16831 + + stricter handing of the Lua DNS update policy + + .. change:: + :tags: Bug Fixes + :pullreq: 17267 + :tickets: 17000 + + correctly delete ENT records from the API + + .. change:: + :tags: Bug Fixes + :pullreq: 17269 + :tickets: 17126 + + lua: one more bad case of createForward + + .. change:: + :tags: Bug Fixes + :pullreq: 17271 + :tickets: 17130 + + minor pdns_control bugfixes + + .. change:: + :tags: Bug Fixes + :pullreq: 17272 + :tickets: 17149 + + webserver: correctly split the basic authorization cookie + + .. change:: + :tags: Bug Fixes + :pullreq: 17274 + :tickets: 17152 + + fixes to AXFR in Bind backend + + .. change:: + :tags: Bug Fixes + :pullreq: 17276 + :tickets: 17155 + + dnsupdate handling buglet + .. changelog:: :version: 4.9.14 :released: 22th of April 2026 diff --git a/docs/changelog/5.0.rst b/docs/changelog/5.0.rst index ad7b1260deb1..fb49065026f8 100644 --- a/docs/changelog/5.0.rst +++ b/docs/changelog/5.0.rst @@ -1,6 +1,105 @@ Changelogs for 5.0.x ==================== +.. changelog:: + :version: 5.0.5 + :released: 20th of May 2026 + + This is release 5.0.5 of the Authoritative Server. + It contains bug fixes and security fixes. + + Please review the :doc:`Upgrade Notes <../upgrading>` before upgrading from versions < 4.9.x. + + .. change:: + :tags: Bug Fixes + :pullreq: 17443 + + Fix PowerDNS Security Advisory 2026-06 for PowerDNS Authoritative Server: Multiple Issues + + .. change:: + :tags: Bug Fixes + :pullreq: 17296 + :tickets: 17284 + + use less inefficient code in web server + + .. change:: + :tags: Bug Fixes + :pullreq: 17294 + :tickets: 17240 + + harden xfr*BitInt writers + + .. change:: + :tags: Bug Fixes + :pullreq: 17259 + :tickets: 16636 + + perform axfr immediately when creating an autosecondary domain + + .. change:: + :tags: Bug Fixes + :pullreq: 17261 + :tickets: 16671 + + Actually install binaries when building with meson + + .. change:: + :tags: Bug Fixes + :pullreq: 17263 + :tickets: 16731 + + web: stricter control of statistics rings changes + + .. change:: + :tags: Bug Fixes + :pullreq: 17264 + :tickets: 16831 + + stricter handing of the Lua DNS update policy + + .. change:: + :tags: Bug Fixes + :pullreq: 17266 + :tickets: 17000 + + correctly delete ENT records from the API + + .. change:: + :tags: Bug Fixes + :pullreq: 17268 + :tickets: 17126 + + lua: one more bad case of createForward + + .. change:: + :tags: Bug Fixes + :pullreq: 17270 + :tickets: 17130 + + minor pdns_control bugfixes + + .. change:: + :tags: Bug Fixes + :pullreq: 17273 + :tickets: 17149 + + webserver: correctly split the basic authorization cookie + + .. change:: + :tags: Bug Fixes + :pullreq: 17275 + :tickets: 17152 + + fixes to AXFR in Bind backend + + .. change:: + :tags: Bug Fixes + :pullreq: 17277 + :tickets: 17155 + + dnsupdate handling buglet + .. changelog:: :version: 5.0.4 :released: 22th of April 2026 diff --git a/docs/secpoll.zone b/docs/secpoll.zone index 57c05d1fde53..6c4cbbf5a4df 100644 --- a/docs/secpoll.zone +++ b/docs/secpoll.zone @@ -1,4 +1,4 @@ -@ 86400 IN SOA pdns-public-ns1.powerdns.com. peter\.van\.dijk.powerdns.com. 2026042901 10800 3600 604800 10800 +@ 86400 IN SOA pdns-public-ns1.powerdns.com. peter\.van\.dijk.powerdns.com. 2026052001 10800 3600 604800 10800 @ 3600 IN NS pdns-public-ns1.powerdns.com. @ 3600 IN NS pdns-public-ns2.powerdns.com. @@ -142,16 +142,18 @@ auth-4.9.10.security-status 60 IN TXT "3 Upgrade now auth-4.9.11.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/authoritative/security-advisories/powerdns-advisory-2026-05.html" auth-4.9.12.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/authoritative/security-advisories/powerdns-advisory-2026-05.html" auth-4.9.13.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/authoritative/security-advisories/powerdns-advisory-2026-05.html" -auth-4.9.14.security-status 60 IN TXT "1 OK" +auth-4.9.14.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/authoritative/security-advisories/powerdns-advisory-2026-06.html" +auth-4.9.15.security-status 60 IN TXT "1 OK" auth-5.0.0-alpha1.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/authoritative/security-advisories/powerdns-advisory-2026-05.html" auth-5.0.0-beta1.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/authoritative/security-advisories/powerdns-advisory-2026-05.html" auth-5.0.0.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/authoritative/security-advisories/powerdns-advisory-2026-05.html" auth-5.0.1.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/authoritative/security-advisories/powerdns-advisory-2026-05.html" auth-5.0.2.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/authoritative/security-advisories/powerdns-advisory-2026-05.html" auth-5.0.3.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/authoritative/security-advisories/powerdns-advisory-2026-05.html" -auth-5.0.4.security-status 60 IN TXT "1 OK" +auth-5.0.4.security-status 60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/authoritative/security-advisories/powerdns-advisory-2026-06.html" +auth-5.0.5.security-status 60 IN TXT "1 OK" auth-5.1.0-alpha1.security-status 60 IN TXT "3 Superseded pre-release (known vulnerabilities)" -auth-5.1.0-beta1.security-status 60 IN TXT "1 Unsupported pre-release (no known vulnerabilities)" +auth-5.1.0-beta1.security-status 60 IN TXT "3 Unsupported pre-release (known vulnerabilities)" ; Auth Debian auth-3.4.1-2.debian.security-status 60 IN TXT "3 Upgrade now, see https://docs.powerdns.com/authoritative/appendices/EOL.html" diff --git a/docs/security-advisories/powerdns-advisory-2026-06.rst b/docs/security-advisories/powerdns-advisory-2026-06.rst new file mode 100644 index 000000000000..30ab88c40ad0 --- /dev/null +++ b/docs/security-advisories/powerdns-advisory-2026-06.rst @@ -0,0 +1,119 @@ +PowerDNS Security Advisory 2026-06: Multiple Issues +=================================================== + +Concurrency and locking defects in GSS-TSIG +------------------------------------------- + +- CVE: CVE-2026-42002 +- Date: 2026-05-06T00:00:00+00:00 +- Affects: PowerDNS Authoritative Server 4.7.0 up to and including 4.9.14 and 5.0.4 +- Not affected: PowerDNS Authoritative Server 4.9.15, 5.0.5 +- Severity: Medium +- Impact: Denial of service +- Exploit: Concurrent TKEY queries for the same key may accidentally share the same GSS-TSIG data structures and cause memory corruption or unexpected server exit. +- Risk of system compromise: None +- Solution: Upgrade to patched version or disable gss-tsig support in server configuration +- CWE: CWE-364 +- CVSS: 3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H +- Last affected: 4.9.14,5.0.4 +- First fixed: 4.9.15,5.0.5 +- Internal ID: 381 + +Multiple concurrency and locking defects in the GSS-TSIG code can lead to +memory corruption due to accidental data structure sharing, which can in turn +lead to a program crash. + +Moreover, the lack of bounds on the number of in-flight GSS-TSIG contexts can +lead to unbounded memory consumption in case of an excessive number of requests +at a given time. A limit of 1000 contexts is now enforced, and can be modified +with the "gss-max-contexts" parameter in server configuration. + +Insufficient Validation of Autoprimary SOA Queries +-------------------------------------------------- + +- CVE: CVE-2026-42001 +- Date: 2026-05-06T00:00:00+00:00 +- Affects: PowerDNS Authoritative Server 4.1.0 up to and including 4.9.14 and 5.0.4 +- Not affected: PowerDNS Authoritative Server 4.9.15, 5.0.5 +- Severity: High +- Impact: Denial of service +- Exploit: Ill-formed answer to SOA query from server operating in autosecondary mode +- Risk of system compromise: None +- Solution: Upgrade to patched version, or disable autosecondary operation +- CWE: CWE-400 +- CVSS: 3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H +- Last affected: 4.9.14,5.0.4 +- First fixed: 4.9.15,5.0.5 +- Internal ID: 467 + +Missing sanity checks of the answer to the initial SOA query, when running in +autosecondary mode and receiving a notification for a not-yet-known domain +may cause the server to crash. + +Insufficient Validation of Names During AXFR +-------------------------------------------- + +- CVE: CVE-2026-42000 +- Date: 2026-05-06T00:00:00+00:00 +- Affects: PowerDNS Authoritative Server up to and including 4.9.14 and 5.0.4 +- Not affected: PowerDNS Authoritative Server 4.9.15, 5.0.5 +- Severity: Medium +- Impact: Denial of service +- Exploit: AXFR of zone with specific contents to Bind backend +- Risk of system compromise: None +- Solution: Upgrade to patched version +- CWE: CWE-77 +- CVSS: 3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:N/I:H/A:N +- Last affected: 4.9.14,5.0.4 +- First fixed: 4.9.15,5.0.5 +- Internal ID: 474 + +Missing escaping of special characters (such as $ or @) in DNS names received +during an AXFR operation can lead to an incorrect (non-parsable) Bind backend +configuration to be written, causing this backend to fail until manual +operation is performed to fix the configuration. + +Incorrect Behaviour of Views with TCP PROXY Requests +---------------------------------------------------- + +- CVE: CVE-2026-41999 +- Date: 2026-05-06T00:00:00+00:00 +- Affects: PowerDNS Authoritative Server 5.0.0 up to and including 5.0.4 +- Not affected: PowerDNS Authoritative Server 5.0.5 +- Severity: Medium +- Impact: Information Disclosure +- Exploit: TCP query using PROXY Protocol +- Risk of system compromise: None +- Solution: Upgrade to patched version or disable views feature +- CWE: CWE-284 +- CVSS: 3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N +- Last affected: 5.0.4 +- First fixed: 5.0.5 +- Internal ID: 482 + +When using views, queries sent using TCP Proxy Protocol will select the view +according to the address of the proxy, rather than the address of the initial +query. This can lead to wrong data being returned. + +Insufficient Validation of Member Zone Data May Cause Catalog Zone Transfer to Fail +----------------------------------------------------------------------------------- + +- CVE: CVE-2026-42396 +- Date: 2026-05-06T00:00:00+00:00 +- Affects: PowerDNS Authoritative Server 4.7.0 up to and including 4.9.14 and 5.0.4 +- Not affected: PowerDNS Authoritative Server 4.9.15, 5.0.5 +- Severity: Medium +- Impact: Denial of service +- Exploit: AXFR of catalog zone with a member whose producer group option +contains a double-quote character +- Risk of system compromise: None +- Solution: Upgrade to patched version, or remove all double-quote characters from producer group names. +- CWE: CWE-94 +- CVSS: 3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H +- Last affected: 4.9.14,5.0.4 +- First fixed: 4.9.15,5.0.5 +- Internal ID: 483 + +Missing proper escaping of double-quote characters when computing labels will +cause AXFR of a catalog zone with a member whose producer group option contains +such a character to fail.