diff --git a/include/znc/HTTPSock.h b/include/znc/HTTPSock.h index 5ec4453c..195b8b6a 100644 --- a/include/znc/HTTPSock.h +++ b/include/znc/HTTPSock.h @@ -83,6 +83,7 @@ class CHTTPSock : public CSocket { const CString& GetPass() const; const CString& GetParamString() const; const CString& GetContentType() const; + const CString& GetURI() const; const CString& GetURIPrefix() const; bool IsPost() const; // !Getters diff --git a/include/znc/Modules.h b/include/znc/Modules.h index 4eaab848..063d0040 100644 --- a/include/znc/Modules.h +++ b/include/znc/Modules.h @@ -477,6 +477,12 @@ class CModule { */ virtual bool OnWebRequest(CWebSock& WebSock, const CString& sPageName, CTemplate& Tmpl); + /** If ValidateWebRequestCSRFCheck returned false, a CSRF error will be printed. + * @param WebSock The active request. + * @param sPageName The name of the page that has been requested. + * @return You MUST return true if the CSRF token is valid. + */ + virtual bool ValidateWebRequestCSRFCheck(CWebSock& WebSock, const CString& sPageName); /** Registers a sub page for the sidebar. * @param spSubPage The SubPage instance. */ diff --git a/include/znc/WebModules.h b/include/znc/WebModules.h index 576cfffc..f2b36b41 100644 --- a/include/znc/WebModules.h +++ b/include/znc/WebModules.h @@ -178,6 +178,9 @@ class CWebSock : public CHTTPSock { static void FinishUserSessions(const CUser& User); + CString GetCSRFCheck(); + bool ValidateCSRFCheck(const CString& sURI); + protected: using CHTTPSock::PrintErrorPage; @@ -186,7 +189,6 @@ class CWebSock : public CHTTPSock { VCString GetDirs(CModule* pModule, bool bIsTemplate); void SetPaths(CModule* pModule, bool bIsTemplate = false); void SetVars(); - CString GetCSRFCheck(); private: EPageReqResult OnPageRequestInternal(const CString& sURI, diff --git a/modules/data/samplewebapi/tmpl/index.tmpl b/modules/data/samplewebapi/tmpl/index.tmpl new file mode 100644 index 00000000..bebddd78 --- /dev/null +++ b/modules/data/samplewebapi/tmpl/index.tmpl @@ -0,0 +1,21 @@ + + +
+
+

Sample Web API

+
+
+
+
Text:
+ +
Sample text that will be returned plain on submit/API request. +
+
+ +
+
+
+
+
+ + diff --git a/modules/samplewebapi.cpp b/modules/samplewebapi.cpp new file mode 100644 index 00000000..6c53ce91 --- /dev/null +++ b/modules/samplewebapi.cpp @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2004-2016 ZNC, see the NOTICE file for details. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +class CSampleWebAPIMod : public CModule { + public: + MODCONSTRUCTOR(CSampleWebAPIMod) {} + + ~CSampleWebAPIMod() override {} + + bool OnWebRequest(CWebSock& WebSock, const CString& sPageName, + CTemplate& Tmpl) override { + if (sPageName != "index") { + // only accept requests to index + return false; + } + + if (WebSock.IsPost()) { + // print the text we just recieved + CString text = WebSock.GetRawParam("text", true); + WebSock.PrintHeader(text.length(), "text/plain; charset=UTF-8"); + WebSock.Write(text); + WebSock.Close(Csock::CLT_AFTERWRITE); + return false; + } + + return true; + } + + bool WebRequiresLogin() { + return false; + } + + bool ValidateWebRequestCSRFCheck(CWebSock& WebSock, + const CString& sPageName) override { + return true; + } +}; + +template <> +void TModInfo(CModInfo& Info) { + Info.SetWikiPage("samplewebapi"); +} + +GLOBALMODULEDEFS(CSampleWebAPIMod, "Sample Web API module.") diff --git a/src/HTTPSock.cpp b/src/HTTPSock.cpp index c2958b62..07b53905 100644 --- a/src/HTTPSock.cpp +++ b/src/HTTPSock.cpp @@ -536,6 +536,8 @@ const CString& CHTTPSock::GetContentType() const { return m_sContentType; } const CString& CHTTPSock::GetParamString() const { return m_sPostData; } +const CString& CHTTPSock::GetURI() const { return m_sURI; } + const CString& CHTTPSock::GetURIPrefix() const { return m_sURIPrefix; } bool CHTTPSock::HasParam(const CString& sName, bool bPost) const { diff --git a/src/Modules.cpp b/src/Modules.cpp index f1bd5e6f..ecd8fb8d 100644 --- a/src/Modules.cpp +++ b/src/Modules.cpp @@ -594,6 +594,10 @@ bool CModule::OnWebRequest(CWebSock& WebSock, const CString& sPageName, CTemplate& Tmpl) { return false; } +bool CModule::ValidateWebRequestCSRFCheck(CWebSock& WebSock, + const CString& sPageName) { + return WebSock.ValidateCSRFCheck(WebSock.GetURI()); +} bool CModule::OnEmbeddedWebRequest(CWebSock& WebSock, const CString& sPageName, CTemplate& Tmpl) { return false; diff --git a/src/WebModules.cpp b/src/WebModules.cpp index f59f0883..b7065886 100644 --- a/src/WebModules.cpp +++ b/src/WebModules.cpp @@ -647,16 +647,17 @@ CWebSock::EPageReqResult CWebSock::OnPageRequestInternal(const CString& sURI, return PAGE_DONE; } - // Check that they really POSTed from one our forms by checking if they + // For pages *not provided* by modules, a CSRF check is performed which involves: + // Ensure that they really POSTed from one our forms by checking if they // know the "secret" CSRF check value. Don't do this for login since // CSRF against the login form makes no sense and the login form does a // cookies-enabled check which would break otherwise. // Don't do this, if user authenticated using http-basic auth, because: // 1. they obviously know the password, // 2. it's easier to automate some tasks e.g. user creation, without need to - // care about cookies and csrf - if (IsPost() && !m_bBasicAuth && - GetParam("_CSRF_Check") != GetCSRFCheck() && sURI != "/login") { + // care about cookies and CSRF + if (IsPost() && !m_bBasicAuth && !sURI.StartsWith("/mods/") && + !ValidateCSRFCheck(sURI)) { DEBUG("Expected _CSRF_Check: " << GetCSRFCheck()); DEBUG("Actual _CSRF_Check: " << GetParam("_CSRF_Check")); PrintErrorPage( @@ -803,6 +804,19 @@ CWebSock::EPageReqResult CWebSock::OnPageRequestInternal(const CString& sURI, if (!pModule) return PAGE_NOTFOUND; + // Pass CSRF check to module. + // Note that the normal CSRF checks are not applied to /mods/ URLs. + if (IsPost() && !m_bBasicAuth && + !pModule->ValidateWebRequestCSRFCheck(*this, m_sPage)) { + DEBUG("Expected _CSRF_Check: " << GetCSRFCheck()); + DEBUG("Actual _CSRF_Check: " << GetParam("_CSRF_Check")); + PrintErrorPage( + 403, "Access denied", + "POST requests need to send " + "a secret token to prevent cross-site request forgery attacks."); + return PAGE_DONE; + } + m_Template["ModPath"] = pModule->GetWebPath(); m_Template["ModFilesPath"] = pModule->GetWebFilesPath(); @@ -969,6 +983,10 @@ CString CWebSock::GetCSRFCheck() { return pSession->GetId().MD5(); } +bool CWebSock::ValidateCSRFCheck(const CString& sURI) { + return sURI == "/login" || GetParam("_CSRF_Check") == GetCSRFCheck(); +} + bool CWebSock::OnLogin(const CString& sUser, const CString& sPass, bool bBasic) { DEBUG("=================== CWebSock::OnLogin(), basic auth? " diff --git a/test/integration/main.cpp b/test/integration/main.cpp index 23656ae3..4a725a75 100644 --- a/test/integration/main.cpp +++ b/test/integration/main.cpp @@ -1965,4 +1965,22 @@ TEST_F(ZNCTest, KeepNickModule) { ":Unable to obtain nick user: Nope :-P, #error"); } +TEST_F(ZNCTest, ModuleCSRFOverride) { + auto znc = Run(); + Z; + auto ircd = ConnectIRCd(); + Z; + auto client = LoginClient(); + Z; + client.Write("znc loadmod samplewebapi"); + client.ReadUntil("Loaded module"); + Z; + auto request = QNetworkRequest(QUrl("http://127.0.0.1:12345/mods/global/samplewebapi/")); + auto reply = HttpPost(request, { + {"text", "ipsum"} + })->readAll().toStdString(); + Z; + EXPECT_THAT(reply, HasSubstr("ipsum")); +} + } // namespace