From f5afbdd4dcd506fd39ddf77c37f7643e67de01e6 Mon Sep 17 00:00:00 2001 From: Vegard Fladby Date: Mon, 27 Jul 2026 08:54:27 +0200 Subject: [PATCH] Make the session cookie unforgeable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The session cookie was a constant: any client that set M2MSESSIONID=1 was treated as logged in, bypassing the password entirely — including for OTA firmware upload. The cookie value is now MD5(client IP + login password), verified on every request, as suggested in the issue discussion. Also marks the cookie HttpOnly/SameSite=Strict and clears it properly on logout. Fixes floatplane/MitsuQTT#59 Co-Authored-By: Claude Fable 5 --- src/main.cpp | 23 ++++++++++++++++++----- src/main.hpp | 1 + 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index e87d999..5171866 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -32,6 +32,7 @@ ESP8266WebServer server(80); // ESP8266 web #include // for Update #include // DNS for captive portal #include // SwiCago library: https://github.com/SwiCago/HeatPump +#include // for the session cookie hash #include #include // MQTT: PubSubClient 2.8.0 @@ -1266,6 +1267,18 @@ void handleLogin() { renderView(views::login, data, {{"header", partials::header}, {"footer", partials::footer}}); } +// The session cookie is a hash of the client's IP address and the login password +// (https://github.com/floatplane/MitsuQTT/issues/59): it can't be forged without knowing the +// password, and can't be replayed from a different address. Changing the password invalidates +// all sessions. +String sessionCookie() { + MD5Builder md5; + md5.begin(); + md5.add(server.client().remoteIP().toString() + config.unit.login_password); + md5.calculate(); + return String(F("M2MSESSIONID=")) + md5.toString(); +} + // Handle the auth via POST // If the password is correct, set the session cookie and redirect to the home page // If the password is incorrect, redirect back to the login page with an error message @@ -1274,12 +1287,12 @@ void handleAuth() { if (server.hasArg("PASSWORD") && server.arg("PASSWORD") == config.unit.login_password) { server.sendHeader("Cache-Control", "no-cache"); - server.sendHeader("Set-Cookie", "M2MSESSIONID=1"); + server.sendHeader("Set-Cookie", sessionCookie() + F("; HttpOnly; SameSite=Strict")); server.sendHeader("Location", "/"); server.send(httpFound, F("text/plain"), "Redirect to home page"); } else { server.sendHeader("Cache-Control", "no-cache"); - server.sendHeader("Set-Cookie", "M2MSESSIONID=0"); + server.sendHeader("Set-Cookie", F("M2MSESSIONID=0; HttpOnly; SameSite=Strict")); server.sendHeader("Location", "/login?authError"); server.send(httpFound, F("text/plain"), "Redirect to login"); } @@ -1290,7 +1303,7 @@ void handleLogout() { LOG(F("handleLogout()")); server.sendHeader("Cache-Control", "no-cache"); - server.sendHeader("Set-Cookie", "M2MSESSIONID=0"); + server.sendHeader("Set-Cookie", F("M2MSESSIONID=0; HttpOnly; SameSite=Strict; Max-Age=0")); server.sendHeader("Location", "/login"); server.send(httpFound, F("text/plain"), "Redirect to login"); } @@ -1947,8 +1960,8 @@ String getId() { // Check if header is present and correct bool is_authenticated() { if (server.hasHeader("Cookie")) { - // Found cookie; - if (server.header("Cookie").indexOf("M2MSESSIONID=1") != -1) { + // Found cookie; verify that it contains the session token for this client + if (server.header("Cookie").indexOf(sessionCookie()) != -1) { // Authentication Successful return true; } diff --git a/src/main.hpp b/src/main.hpp index 682c681..10be70d 100644 --- a/src/main.hpp +++ b/src/main.hpp @@ -72,5 +72,6 @@ bool connectWifi(); bool checkLogin(); HeatpumpSettings change_states(const HeatpumpSettings &settings); String getTemperatureScale(); +String sessionCookie(); bool is_authenticated(); void hpCheckRemoteTemp(); \ No newline at end of file