Portal: HTML-escape the SSID and WiFi name reflected into the setup pages (#24)

Escape SSIDs/WiFi names reflected into the setup portal (attribute uses single quotes, which jesc() didn't cover).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Александр М.andClaude Opus 5.5 authored and GitHub committed 2026-10-06 13:32:59 +03:00
1 parent 19473839c2
commit 014c7ed969
1 file changed
+22 -2
+22 -2
View File
@@ -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 += "&amp;"; break;
case '<': o += "&lt;"; break;
case '>': o += "&gt;"; break;
case '"': o += "&quot;"; break;
case '\'': o += "&#39;"; 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", "<!doctype html><meta charset=utf-8><body style='font:16px system-ui;text-align:center;margin-top:60px'>"
"&#9989; Saved. Restarting and joining <b>" + ss + "</b>&hellip;<br><br>"
"&#9989; Saved. Restarting and joining <b>" + htmlEscape(ss) + "</b>&hellip;<br><br>"
"Reconnect your phone to your normal WiFi, then find the box at <b>c3adblock.local</b>.</body>");
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 += "<option value='" + jesc(WiFi.SSID(i)) + "'>";
for (int i = 0; i < n && i < 15; i++) portalOpts += "<option value='" + htmlEscape(WiFi.SSID(i)) + "'>";
uint8_t mac[6]; WiFi.macAddress(mac);
char ap[24]; snprintf(ap, sizeof(ap), "C3-AdBlock-%02X%02X", mac[4], mac[5]);
WiFi.mode(WIFI_AP); WiFi.softAP(ap);