From 450c6401489287d6db77212cb0f36cf8d2f743a5 Mon Sep 17 00:00:00 2001 From: me-no-dev Date: Mon, 30 Jun 2025 14:25:41 +0300 Subject: [PATCH 1/4] fix(csrf): Fix SCRF vulnerability in WebUpdate.ino --- .../examples/WebUpdate/WebUpdate.ino | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/libraries/WebServer/examples/WebUpdate/WebUpdate.ino b/libraries/WebServer/examples/WebUpdate/WebUpdate.ino index 10ddb5e7b64..bc5c39519c6 100644 --- a/libraries/WebServer/examples/WebUpdate/WebUpdate.ino +++ b/libraries/WebServer/examples/WebUpdate/WebUpdate.ino @@ -11,11 +11,16 @@ const char *host = "esp32-webupdate"; const char *ssid = "........"; const char *password = "........"; +const char * authUser = "admin"; +const char * authPass = "admin"; WebServer server(80); const char *serverIndex = "
"; +const char * csrfHeaders[2] = {"Origin", "Host"}; +static bool authenticated = false; + void setup(void) { Serial.begin(115200); Serial.println(); @@ -24,37 +29,62 @@ void setup(void) { WiFi.begin(ssid, password); if (WiFi.waitForConnectResult() == WL_CONNECTED) { MDNS.begin(host); + server.collectHeaders(csrfHeaders, 2); server.on("/", HTTP_GET, []() { + if (!server.authenticate(authUser, authPass)) { + return server.requestAuthentication(); + } server.sendHeader("Connection", "close"); server.send(200, "text/html", serverIndex); }); server.on( "/update", HTTP_POST, []() { + if (!authenticated) { + return server.requestAuthentication(); + } server.sendHeader("Connection", "close"); - server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK"); - ESP.restart(); + if (Update.hasError()) { + server.send(200, "text/plain", "FAIL"); + } else { + server.send(200, "text/plain", "Success! Rebooting..."); + delay(500); + ESP.restart(); + } }, []() { HTTPUpload &upload = server.upload(); if (upload.status == UPLOAD_FILE_START) { Serial.setDebugOutput(true); + authenticated = server.authenticate(authUser, authPass); + if (!authenticated) { + Serial.println("Authentication fail!"); + return; + } + String origin = server.header(String(csrfHeaders[0])); + String host = server.header(String(csrfHeaders[1])); + String expectedOrigin = String("http://") + host; + if (origin != expectedOrigin) { + Serial.printf("Wrong origin received! Expected: %s, Received: %s\n", expectedOrigin.c_str(), origin.c_str()); + authenticated = false; + } + Serial.printf("Update: %s\n", upload.filename.c_str()); if (!Update.begin()) { //start with max available size Update.printError(Serial); } - } else if (upload.status == UPLOAD_FILE_WRITE) { + } else if (authenticated && upload.status == UPLOAD_FILE_WRITE) { if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { Update.printError(Serial); } - } else if (upload.status == UPLOAD_FILE_END) { + } else if (authenticated && upload.status == UPLOAD_FILE_END) { if (Update.end(true)) { //true to set the size to the current progress Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize); } else { Update.printError(Serial); } Serial.setDebugOutput(false); - } else { + } else if(authenticated) { Serial.printf("Update Failed Unexpectedly (likely broken connection): status=%d\n", upload.status); } } From a87ee5fd38b3348e785b2f03a37e6b1e2e6c7ba8 Mon Sep 17 00:00:00 2001 From: me-no-dev Date: Mon, 30 Jun 2025 15:50:30 +0300 Subject: [PATCH 2/4] fix(csrf): Prevent CSRF on other OTA examples --- .../HTTPUpdateServer/src/HTTPUpdateServer.h | 19 +++++++++++ .../examples/OTAWebUpdater/OTAWebUpdater.ino | 33 +++++++++++++++++-- .../examples/WebUpdate/WebUpdate.ino | 1 + 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/libraries/HTTPUpdateServer/src/HTTPUpdateServer.h b/libraries/HTTPUpdateServer/src/HTTPUpdateServer.h index bb32bc03fdb..d45ad2cda68 100644 --- a/libraries/HTTPUpdateServer/src/HTTPUpdateServer.h +++ b/libraries/HTTPUpdateServer/src/HTTPUpdateServer.h @@ -27,6 +27,7 @@ static const char serverIndex[] PROGMEM = )"; static const char successResponse[] PROGMEM = "Update Success! Rebooting..."; +static const char * csrfHeaders[2] = {"Origin", "Host"}; class HTTPUpdateServer { public: @@ -56,6 +57,9 @@ class HTTPUpdateServer { _username = username; _password = password; + // collect headers for CSRF verification + _server->collectHeaders(csrfHeaders, 2); + // handler for the /update form page _server->on(path.c_str(), HTTP_GET, [&]() { if (_username != emptyString && _password != emptyString && !_server->authenticate(_username.c_str(), _password.c_str())) { @@ -69,6 +73,10 @@ class HTTPUpdateServer { path.c_str(), HTTP_POST, [&]() { if (!_authenticated) { + if (_username == emptyString || _password == emptyString) { + _server->send(200, F("text/html"), String(F("Update error: Wrong origin received!"))); + return; + } return _server->requestAuthentication(); } if (Update.hasError()) { @@ -100,6 +108,17 @@ class HTTPUpdateServer { return; } + String origin = _server->header(String(csrfHeaders[0])); + String host = _server->header(String(csrfHeaders[1])); + String expectedOrigin = String("http://") + host; + if (origin != expectedOrigin) { + if (_serial_output) { + Serial.printf("Wrong origin received! Expected: %s, Received: %s\n", expectedOrigin.c_str(), origin.c_str()); + } + _authenticated = false; + return; + } + if (_serial_output) { Serial.printf("Update: %s\n", upload.filename.c_str()); } diff --git a/libraries/Update/examples/OTAWebUpdater/OTAWebUpdater.ino b/libraries/Update/examples/OTAWebUpdater/OTAWebUpdater.ino index 7059bef4496..4f15f6701c7 100644 --- a/libraries/Update/examples/OTAWebUpdater/OTAWebUpdater.ino +++ b/libraries/Update/examples/OTAWebUpdater/OTAWebUpdater.ino @@ -7,11 +7,16 @@ #define SSID_FORMAT "ESP32-%06lX" // 12 chars total //#define PASSWORD "test123456" // generate if remarked +const char * authUser = "admin"; +const char * authPass = "admin"; WebServer server(80); Ticker tkSecond; uint8_t otaDone = 0; +const char * csrfHeaders[2] = {"Origin", "Host"}; +static bool authenticated = false; + const char *alphanum = "0123456789!@#$%^&*abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; String generatePass(uint8_t str_len) { String buff; @@ -38,6 +43,9 @@ void apMode() { } void handleUpdateEnd() { + if (!authenticated) { + return server.requestAuthentication(); + } server.sendHeader("Connection", "close"); if (Update.hasError()) { server.send(502, "text/plain", Update.errorString()); @@ -45,6 +53,7 @@ void handleUpdateEnd() { server.sendHeader("Refresh", "10"); server.sendHeader("Location", "/"); server.send(307); + delay(500); ESP.restart(); } } @@ -56,18 +65,34 @@ void handleUpdate() { } HTTPUpload &upload = server.upload(); if (upload.status == UPLOAD_FILE_START) { + authenticated = server.authenticate(authUser, authPass); + if (!authenticated) { + Serial.println("Authentication fail!"); + otaDone = 0; + return; + } + String origin = server.header(String(csrfHeaders[0])); + String host = server.header(String(csrfHeaders[1])); + String expectedOrigin = String("http://") + host; + if (origin != expectedOrigin) { + Serial.printf("Wrong origin received! Expected: %s, Received: %s\n", expectedOrigin.c_str(), origin.c_str()); + authenticated = false; + otaDone = 0; + return; + } + Serial.printf("Receiving Update: %s, Size: %d\n", upload.filename.c_str(), fsize); if (!Update.begin(fsize)) { otaDone = 0; Update.printError(Serial); } - } else if (upload.status == UPLOAD_FILE_WRITE) { + } else if (authenticated && upload.status == UPLOAD_FILE_WRITE) { if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { Update.printError(Serial); } else { otaDone = 100 * Update.progress() / Update.size(); } - } else if (upload.status == UPLOAD_FILE_END) { + } else if (authenticated && upload.status == UPLOAD_FILE_END) { if (Update.end(true)) { Serial.printf("Update Success: %u bytes\nRebooting...\n", upload.totalSize); } else { @@ -78,6 +103,7 @@ void handleUpdate() { } void webServerInit() { + server.collectHeaders(csrfHeaders, 2); server.on( "/update", HTTP_POST, []() { @@ -92,6 +118,9 @@ void webServerInit() { server.send_P(200, "image/x-icon", favicon_ico_gz, favicon_ico_gz_len); }); server.onNotFound([]() { + if (!server.authenticate(authUser, authPass)) { + return server.requestAuthentication(); + } server.send(200, "text/html", indexHtml); }); server.begin(); diff --git a/libraries/WebServer/examples/WebUpdate/WebUpdate.ino b/libraries/WebServer/examples/WebUpdate/WebUpdate.ino index bc5c39519c6..b27d5ab1d16 100644 --- a/libraries/WebServer/examples/WebUpdate/WebUpdate.ino +++ b/libraries/WebServer/examples/WebUpdate/WebUpdate.ino @@ -67,6 +67,7 @@ void setup(void) { if (origin != expectedOrigin) { Serial.printf("Wrong origin received! Expected: %s, Received: %s\n", expectedOrigin.c_str(), origin.c_str()); authenticated = false; + return; } Serial.printf("Update: %s\n", upload.filename.c_str()); From b29b5e8bdbd93bcadcbdf6f7c68d3ee0707c4114 Mon Sep 17 00:00:00 2001 From: me-no-dev Date: Wed, 2 Jul 2025 10:40:12 +0300 Subject: [PATCH 3/4] fix(csrf): Require auth user and pass to be changed --- libraries/Update/examples/OTAWebUpdater/OTAWebUpdater.ino | 6 ++++-- libraries/WebServer/examples/WebUpdate/WebUpdate.ino | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/libraries/Update/examples/OTAWebUpdater/OTAWebUpdater.ino b/libraries/Update/examples/OTAWebUpdater/OTAWebUpdater.ino index 4f15f6701c7..c4d447e4b80 100644 --- a/libraries/Update/examples/OTAWebUpdater/OTAWebUpdater.ino +++ b/libraries/Update/examples/OTAWebUpdater/OTAWebUpdater.ino @@ -7,8 +7,10 @@ #define SSID_FORMAT "ESP32-%06lX" // 12 chars total //#define PASSWORD "test123456" // generate if remarked -const char * authUser = "admin"; -const char * authPass = "admin"; + +// Set the username and password for firmware upload +const char * authUser = "........"; +const char * authPass = "........"; WebServer server(80); Ticker tkSecond; diff --git a/libraries/WebServer/examples/WebUpdate/WebUpdate.ino b/libraries/WebServer/examples/WebUpdate/WebUpdate.ino index b27d5ab1d16..fcb2ee22d4b 100644 --- a/libraries/WebServer/examples/WebUpdate/WebUpdate.ino +++ b/libraries/WebServer/examples/WebUpdate/WebUpdate.ino @@ -11,8 +11,10 @@ const char *host = "esp32-webupdate"; const char *ssid = "........"; const char *password = "........"; -const char * authUser = "admin"; -const char * authPass = "admin"; + +// Set the username and password for firmware upload +const char * authUser = "........"; +const char * authPass = "........"; WebServer server(80); const char *serverIndex = From c94ffcf36241831edf84d11cd2e9400059d9abed Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 11:19:02 +0000 Subject: [PATCH 4/4] ci(pre-commit): Apply automatic fixes --- libraries/HTTPUpdateServer/src/HTTPUpdateServer.h | 2 +- .../Update/examples/OTAWebUpdater/OTAWebUpdater.ino | 6 +++--- libraries/WebServer/examples/WebUpdate/WebUpdate.ino | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/libraries/HTTPUpdateServer/src/HTTPUpdateServer.h b/libraries/HTTPUpdateServer/src/HTTPUpdateServer.h index d45ad2cda68..65d8cbaa783 100644 --- a/libraries/HTTPUpdateServer/src/HTTPUpdateServer.h +++ b/libraries/HTTPUpdateServer/src/HTTPUpdateServer.h @@ -27,7 +27,7 @@ static const char serverIndex[] PROGMEM = )"; static const char successResponse[] PROGMEM = "Update Success! Rebooting..."; -static const char * csrfHeaders[2] = {"Origin", "Host"}; +static const char *csrfHeaders[2] = {"Origin", "Host"}; class HTTPUpdateServer { public: diff --git a/libraries/Update/examples/OTAWebUpdater/OTAWebUpdater.ino b/libraries/Update/examples/OTAWebUpdater/OTAWebUpdater.ino index c4d447e4b80..39d6cbce4af 100644 --- a/libraries/Update/examples/OTAWebUpdater/OTAWebUpdater.ino +++ b/libraries/Update/examples/OTAWebUpdater/OTAWebUpdater.ino @@ -9,14 +9,14 @@ //#define PASSWORD "test123456" // generate if remarked // Set the username and password for firmware upload -const char * authUser = "........"; -const char * authPass = "........"; +const char *authUser = "........"; +const char *authPass = "........"; WebServer server(80); Ticker tkSecond; uint8_t otaDone = 0; -const char * csrfHeaders[2] = {"Origin", "Host"}; +const char *csrfHeaders[2] = {"Origin", "Host"}; static bool authenticated = false; const char *alphanum = "0123456789!@#$%^&*abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; diff --git a/libraries/WebServer/examples/WebUpdate/WebUpdate.ino b/libraries/WebServer/examples/WebUpdate/WebUpdate.ino index fcb2ee22d4b..9e45de7d985 100644 --- a/libraries/WebServer/examples/WebUpdate/WebUpdate.ino +++ b/libraries/WebServer/examples/WebUpdate/WebUpdate.ino @@ -13,14 +13,14 @@ const char *ssid = "........"; const char *password = "........"; // Set the username and password for firmware upload -const char * authUser = "........"; -const char * authPass = "........"; +const char *authUser = "........"; +const char *authPass = "........"; WebServer server(80); const char *serverIndex = "
"; -const char * csrfHeaders[2] = {"Origin", "Host"}; +const char *csrfHeaders[2] = {"Origin", "Host"}; static bool authenticated = false; void setup(void) { @@ -71,7 +71,7 @@ void setup(void) { authenticated = false; return; } - + Serial.printf("Update: %s\n", upload.filename.c_str()); if (!Update.begin()) { //start with max available size Update.printError(Serial); @@ -87,7 +87,7 @@ void setup(void) { Update.printError(Serial); } Serial.setDebugOutput(false); - } else if(authenticated) { + } else if (authenticated) { Serial.printf("Update Failed Unexpectedly (likely broken connection): status=%d\n", upload.status); } }