From 5e7ff22cc125a0bc325ba78fdcdc1fc10d14da23 Mon Sep 17 00:00:00 2001 From: MrIron Date: Thu, 6 Aug 2026 13:42:46 +0200 Subject: [PATCH 1/4] Fixed wrong command name after merge. --- mod.cservice/STATUSCommand.cc | 3 ++- mod.cservice/WCommand.cc | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mod.cservice/STATUSCommand.cc b/mod.cservice/STATUSCommand.cc index 2138935c..32bfae82 100644 --- a/mod.cservice/STATUSCommand.cc +++ b/mod.cservice/STATUSCommand.cc @@ -130,7 +130,8 @@ bool STATUSCommand::Exec(iClient* theClient, const string& Message) { } #ifdef THERETURN_ENABLED - if (theChan->hasW()) { + if (theChan->hasW() && + (bot->getConfigVar("THERETURN_INTEGRATION")->asInt() != 0 || admLevel >= 750)) { bot->Notice(theClient, "The channel is also registered with %s.", bot->getConfwNickName().c_str()); } diff --git a/mod.cservice/WCommand.cc b/mod.cservice/WCommand.cc index ce8ce203..e0afca30 100644 --- a/mod.cservice/WCommand.cc +++ b/mod.cservice/WCommand.cc @@ -223,7 +223,7 @@ bool WCommand::Exec(iClient* theClient, const string& Message) { bot->Write(xQuery); return true; - } else if (Command == "PURGE") { + } else if (Command == "XPURGE") { /* Admin? */ if (admLevel < 750) { bot->Notice( @@ -251,7 +251,7 @@ bool WCommand::Exec(iClient* theClient, const string& Message) { } else { bot->Notice(theClient, "%s is flagged as registered with %s in my records. To manually update my " - "records, use /msg X W PURGE %s -f to force unregistration. This should " + "records, use /msg X W XPURGE %s -f to force unregistration. This should " "ONLY be used if %s is already purged with %s", theChan->getName().c_str(), wClient->getNickName().c_str(), theChan->getName().c_str(), theChan->getName().c_str(), From 7ff3da34987d1ad88974597ffcd03a5e2d943f64 Mon Sep 17 00:00:00 2001 From: MrIron Date: Fri, 14 Aug 2026 00:27:31 +0200 Subject: [PATCH 2/4] Added missing param to response message --- mod.cservice/WCommand.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod.cservice/WCommand.cc b/mod.cservice/WCommand.cc index e0afca30..57cb96af 100644 --- a/mod.cservice/WCommand.cc +++ b/mod.cservice/WCommand.cc @@ -203,7 +203,7 @@ bool WCommand::Exec(iClient* theClient, const string& Message) { /* Is W on the channel? */ if (!wChanUser) { bot->Notice(theClient, bot->getResponse(theUser, language::cant_find_on_chan).c_str(), - wClient->getNickName().c_str()); + wClient->getNickName().c_str(), theChan->getName().c_str()); return false; } From d5325b1a76ebf31a1c2d4763fbe7bb176bef909f Mon Sep 17 00:00:00 2001 From: MrIron Date: Sun, 30 Aug 2026 19:56:42 +0200 Subject: [PATCH 3/4] Fix SQL injection in CERT and HELLO commands User-supplied input was concatenated into SQL statements without escaping, allowing quote breakout. Since the DB layer uses PQexec (which permits stacked statements), this enabled arbitrary SQL. - CERTCommand: escape note, fingerprint and nick!user@host in the users_fingerprints INSERT. note is free text from the user and was the primary injectable field. - HELLOCommand: escape the e-mail address in the duplicate-address lookup. Validation permitted a single quote, so the query could be broken out of. --- mod.cservice/CERTCommand.cc | 5 +++-- mod.cservice/HELLOCommand.cc | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mod.cservice/CERTCommand.cc b/mod.cservice/CERTCommand.cc index 581d467b..f369a5b2 100644 --- a/mod.cservice/CERTCommand.cc +++ b/mod.cservice/CERTCommand.cc @@ -160,9 +160,10 @@ bool CERTCommand::Exec([[maybe_unused]] iClient* theClient, std::stringstream theQuery; theQuery << "INSERT INTO users_fingerprints (user_id, fingerprint, added_ts, added_by, " "note) VALUES (" - << theClient->getAccountID() << ", '" << fingerPrint + << theClient->getAccountID() << ", '" << escapeSQLChars(fingerPrint) << "', date_part('epoch', CURRENT_TIMESTAMP)::int, '" - << theClient->getRealNickUserHost() << "', '" << note << "')" << std::endl; + << escapeSQLChars(theClient->getRealNickUserHost()) << "', '" + << escapeSQLChars(note) << "')" << std::endl; if (!bot->SQLDb->Exec(theQuery, true)) { LOGSQL_ERROR(bot->SQLDb); diff --git a/mod.cservice/HELLOCommand.cc b/mod.cservice/HELLOCommand.cc index 801123c7..8e9c5b17 100644 --- a/mod.cservice/HELLOCommand.cc +++ b/mod.cservice/HELLOCommand.cc @@ -131,7 +131,8 @@ bool HELLOCommand::Exec(iClient* theClient, const string& Message) { * Ensure this e-mail address is not already used */ stringstream theQuery; - theQuery << "SELECT id FROM users WHERE lower(email) = '" << st[2] << "'" << ends; + theQuery << "SELECT id FROM users WHERE lower(email) = '" << escapeSQLChars(st[2]) << "'" + << ends; if (!bot->SQLDb->Exec(theQuery, true)) { LOG(ERROR, "SQL error on HELLOCommand, matching e-mail address"); LOGSQL_ERROR(bot->SQLDb); From b44a440c1a962b4ecdaa4bbed74afbbc92ee26b1 Mon Sep 17 00:00:00 2001 From: MrIron Date: Sun, 30 Aug 2026 20:08:16 +0200 Subject: [PATCH 4/4] Fix crashes on malformed SASL SCRAM and OPLIST input Both paths indexed StringTokenizer without a size check; getToken() asserts on an out-of-range subscript, aborting the whole process. - SASL SCRAM client-first: a "n=" attribute with an empty value produced a zero-token tokenizer, and st3[0] aborted before the username-empty check. This is reachable pre-auth from a connecting user's AUTHENTICATE data. Guard st3 before indexing; an empty value now falls through to the existing "missing username" error path. - doXROplist: st[1]/st[2] were read before any size check (a 2-token OPLIST reply aborted), and the guard only required 6 tokens while the body reads up to st[8]. Check size >= 3 before the "NO" test and size >= 9 before the field extraction. --- mod.cservice/cservice.cc | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/mod.cservice/cservice.cc b/mod.cservice/cservice.cc index b225cb66..b7573d0e 100644 --- a/mod.cservice/cservice.cc +++ b/mod.cservice/cservice.cc @@ -7962,9 +7962,11 @@ bool cservice::doXQSASL(iServer* theServer, const string& Routing, const string& for (size_t i = 0; i < st2.size(); ++i) { if (st2[i].rfind("n=", 0) == 0) { StringTokenizer st3(st2[i].substr(2)); - it->username = st3[0]; - if (st3.size() > 1) - it->password = st3[1]; + if (!st3.empty()) { + it->username = st3[0]; + if (st3.size() > 1) + it->password = st3[1]; + } } else if (st2[i].rfind("r=", 0) == 0) it->client_nonce = st2[i].substr(2); } @@ -8631,12 +8633,19 @@ bool cservice::doXROplist(iServer* /*theServer*/, const string& Routing, const s LOG(TRACE, "XQ-OPLIST: Routing: {} Message: {}", Routing, Message); StringTokenizer st(Message); + /* Need at least "OPLIST " before inspecting st[1]/st[2]. */ + if (st.size() < 3) { + LOG(ERROR, "OPLIST insufficient response parameters"); + return false; + } + if (st[2] == "NO") { LOG(TRACE, "NO oplist reported for channel {}", st[1]); return true; } - if (st.size() < 6) { + /* A full OPLIST reply is accessed up to st[8] below. */ + if (st.size() < 9) { LOG(ERROR, "OPLIST insufficient response parameters"); return false; }