diff --git a/src/main.cpp b/src/main.cpp index 92077d3..63b1db4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -294,6 +294,26 @@ static bool handleDns() { // ---------- web ---------- static String macStr(const uint8_t* m) { char s[18]; snprintf(s, sizeof(s), "%02x:%02x:%02x:%02x:%02x:%02x", m[0],m[1],m[2],m[3],m[4],m[5]); return String(s); } static String jesc(const String& s) { String o; for (char ch : s) { if (ch == '"' || ch == '\\') o += '\\'; o += ch; } return o; } +// HTML text/attribute escaping for the setup portal. jesc() covers JSON (stats +// endpoint); the portal builds HTML, and its inputs — a scanned SSID, the +// submitted WiFi name — are attacker-controllable during provisioning (the +// portal AP is open, and a nearby attacker can also broadcast an SSID of their +// choosing). Without escaping both, a crafted SSID/name is reflected script +// into the setup page, the same class as the dashboard XSS fixed earlier. +static String htmlEscape(const String& s) { + String o; o.reserve(s.length()); + for (char ch : s) { + switch (ch) { + case '&': o += "&"; break; + case '<': o += "<"; break; + case '>': o += ">"; break; + case '"': o += """; break; + case '\'': o += "'"; break; + default: o += ch; + } + } + return o; +} #include "page.h" // dashboard HTML (PROGMEM) — see issue #6 @@ -514,7 +534,7 @@ static void handleWifiSave() { if (!ss.length()) { web.send(400, "text/plain", "missing WiFi name"); return; } prefs.begin("wifi", false); prefs.putString("ssid", ss); prefs.putString("pass", pw); prefs.end(); web.send(200, "text/html", "" - "✅ Saved. Restarting and joining " + ss + "…

" + "✅ Saved. Restarting and joining " + htmlEscape(ss) + "…

" "Reconnect your phone to your normal WiFi, then find the box at c3adblock.local."); delay(900); ESP.restart(); } @@ -522,7 +542,7 @@ static void handleWifiSave() { static void startConfigPortal() { int n = WiFi.scanNetworks(); // scan while still in STA mode (no APSTA) portalOpts = ""; - for (int i = 0; i < n && i < 15; i++) portalOpts += "