From 8dc96ae08c15c75ca13af14580b6ea0ab6e31330 Mon Sep 17 00:00:00 2001 From: Alhuda Khan Date: Mon, 26 Oct 2026 13:24:00 +0530 Subject: [PATCH] web: only delete channels, notes, keys and certs on POST --- modules/cert.cpp | 7 ++- modules/certauth.cpp | 4 +- modules/data/cert/tmpl/index.tmpl | 5 ++- modules/data/certauth/tmpl/index.tmpl | 6 ++- modules/data/notes/tmpl/index.tmpl | 6 ++- modules/data/webadmin/tmpl/del_chan.tmpl | 27 ++++++++++++ modules/notes.cpp | 4 +- modules/webadmin.cpp | 19 ++++++-- test/integration/tests/modules.cpp | 56 ++++++++++++++++++++++++ 9 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 modules/data/webadmin/tmpl/del_chan.tmpl diff --git a/modules/cert.cpp b/modules/cert.cpp index 7584068a..d8549e24 100644 --- a/modules/cert.cpp +++ b/modules/cert.cpp @@ -79,7 +79,8 @@ class CCertMod : public CModule { } else if (sPageName == "update") { CFile fPemFile(PemFile()); - if (fPemFile.Open(O_WRONLY | O_TRUNC | O_CREAT)) { + if (WebSock.IsPost() && + fPemFile.Open(O_WRONLY | O_TRUNC | O_CREAT)) { fPemFile.Write(WebSock.GetParam("cert", true, "")); fPemFile.Close(); } @@ -87,7 +88,9 @@ class CCertMod : public CModule { WebSock.Redirect(GetWebPath()); return true; } else if (sPageName == "delete") { - CFile::Delete(PemFile()); + if (WebSock.IsPost()) { + CFile::Delete(PemFile()); + } WebSock.Redirect(GetWebPath()); return true; } diff --git a/modules/certauth.cpp b/modules/certauth.cpp index e8756575..6fb3bc75 100644 --- a/modules/certauth.cpp +++ b/modules/certauth.cpp @@ -375,7 +375,9 @@ class CSSLClientCertMod : public CModule { WebSock.Redirect(GetWebPath()); return true; } else if (sPageName == "delete") { - DelKey(*pUser, WebSock.GetParam("key", false)); + if (WebSock.IsPost()) { + DelKey(*pUser, WebSock.GetParam("key")); + } WebSock.Redirect(GetWebPath()); return true; } diff --git a/modules/data/cert/tmpl/index.tmpl b/modules/data/cert/tmpl/index.tmpl index d3a12cee..cd5d18cb 100644 --- a/modules/data/cert/tmpl/index.tmpl +++ b/modules/data/cert/tmpl/index.tmpl @@ -2,7 +2,10 @@ - +
+ +
+" />

diff --git a/modules/data/certauth/tmpl/index.tmpl b/modules/data/certauth/tmpl/index.tmpl index 3b0d69ab..815c2871 100644 --- a/modules/data/certauth/tmpl/index.tmpl +++ b/modules/data/certauth/tmpl/index.tmpl @@ -33,7 +33,11 @@ - [] +
+ + + " /> +
diff --git a/modules/data/notes/tmpl/index.tmpl b/modules/data/notes/tmpl/index.tmpl index e4087ca3..eea3dd10 100644 --- a/modules/data/notes/tmpl/index.tmpl +++ b/modules/data/notes/tmpl/index.tmpl @@ -38,7 +38,11 @@ - <? FORMAT " /> +
+ + + " /> +
diff --git a/modules/data/webadmin/tmpl/del_chan.tmpl b/modules/data/webadmin/tmpl/del_chan.tmpl new file mode 100644 index 00000000..a54dd8fb --- /dev/null +++ b/modules/data/webadmin/tmpl/del_chan.tmpl @@ -0,0 +1,27 @@ + + + +
+

+
+
+
+ +
+ + + + +
"/>
+
+
+ + +
"/>
+
+
+
+
+
+ + diff --git a/modules/notes.cpp b/modules/notes.cpp index de7d02d0..ad6c237d 100644 --- a/modules/notes.cpp +++ b/modules/notes.cpp @@ -203,7 +203,9 @@ class CNotesMod : public CModule { return true; } else if (sPageName == "delnote") { - DelNote(WebSock.GetParam("key", false)); + if (WebSock.IsPost()) { + DelNote(WebSock.GetParam("key")); + } WebSock.Redirect(GetWebPath()); return true; } else if (sPageName == "addnote") { diff --git a/modules/webadmin.cpp b/modules/webadmin.cpp index 666db814..162df4fb 100644 --- a/modules/webadmin.cpp +++ b/modules/webadmin.cpp @@ -568,7 +568,7 @@ class CWebAdminMod : public CModule { } if (pNetwork) { - return DelChan(WebSock, pNetwork); + return DelChan(WebSock, pNetwork, Tmpl); } WebSock.PrintErrorPage(t_s("No such user or network")); @@ -1306,8 +1306,11 @@ class CWebAdminMod : public CModule { return false; } - bool DelChan(CWebSock& WebSock, CIRCNetwork* pNetwork) { - CString sChan = WebSock.GetParam("name", false); + bool DelChan(CWebSock& WebSock, CIRCNetwork* pNetwork, CTemplate& Tmpl) { + CString sChan = WebSock.GetParam("name"); + if (sChan.empty() && !WebSock.IsPost()) { + sChan = WebSock.GetParam("name", false); + } if (sChan.empty()) { WebSock.PrintErrorPage( @@ -1315,6 +1318,16 @@ class CWebAdminMod : public CModule { return true; } + if (!WebSock.IsPost()) { + // Show the "Are you sure?" page: + + Tmpl.SetFile("del_chan.tmpl"); + Tmpl["Username"] = pNetwork->GetUser()->GetUsername(); + Tmpl["Network"] = pNetwork->GetName(); + Tmpl["Channel"] = sChan; + return true; + } + pNetwork->DelChan(sChan); pNetwork->PutIRC("PART " + sChan); diff --git a/test/integration/tests/modules.cpp b/test/integration/tests/modules.cpp index bc278c3b..2f7a9574 100644 --- a/test/integration/tests/modules.cpp +++ b/test/integration/tests/modules.cpp @@ -443,6 +443,62 @@ TEST_F(ZNCTest, ModuleCSRFOverride) { EXPECT_THAT(reply, HasSubstr("ipsum")); } +TEST_F(ZNCTest, WebDeleteNeedsPost) { + int port = PickPortNumber(); + auto znc = Run(); + auto ircd = ConnectIRCd(); + auto client = LoginClient(); + client.Write(QStringLiteral("znc addport %1 all all").arg(port).toUtf8()); + client.ReadUntil(":Port added"); + client.Write("znc loadmod notes"); + client.ReadUntil("Loaded module"); + client.Write("PRIVMSG *notes :Add secretkey lorem"); + client.ReadUntil("Added note"); + + auto url = [&](const QString& path) { + QNetworkRequest request( + QUrl(QStringLiteral("http://127.0.0.1:%1%2").arg(port).arg(path))); + request.setRawHeader("Authorization", + "Basic " + QByteArray("user:hunter2").toBase64()); + return request; + }; + HttpPost(url("/mods/global/webadmin/addchan"), { + {"user", "user"}, + {"network", "test"}, + {"submitted", "1"}, + {"name", "#znc"}, + {"enabled", "1"}, + }); + const QString sEditNetwork = + "/mods/global/webadmin/editnetwork?user=user&network=test"; + const std::string sChanRow = "name=\"channel\" value=\"#znc\""; + EXPECT_THAT(HttpGet(url(sEditNetwork))->readAll().toStdString(), + HasSubstr(sChanRow)); + + // A plain GET, e.g. from an on another page, must not delete + EXPECT_THAT(HttpGet(url("/mods/global/webadmin/" + "delchan?user=user&network=test&name=%23znc")) + ->readAll() + .toStdString(), + HasSubstr("Confirm Channel Deletion")); + EXPECT_THAT(HttpGet(url(sEditNetwork))->readAll().toStdString(), + HasSubstr(sChanRow)); + HttpGet(url("/mods/user/notes/delnote?key=secretkey")); + EXPECT_THAT(HttpGet(url("/mods/user/notes/"))->readAll().toStdString(), + HasSubstr("secretkey")); + + HttpPost(url("/mods/global/webadmin/delchan"), { + {"user", "user"}, + {"network", "test"}, + {"name", "#znc"}, + }); + EXPECT_THAT(HttpGet(url(sEditNetwork))->readAll().toStdString(), + Not(HasSubstr(sChanRow))); + HttpPost(url("/mods/user/notes/delnote"), {{"key", "secretkey"}}); + EXPECT_THAT(HttpGet(url("/mods/user/notes/"))->readAll().toStdString(), + Not(HasSubstr("secretkey"))); +} + class SaslModuleTest : public ZNCTest, public testing::WithParamInterface< std::pair>> {