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:
1 parent
19473839c2
commit
014c7ed969
1 file changed
+22
-2
+22
-2
@@ -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", "<!doctype html><meta charset=utf-8><body style='font:16px system-ui;text-align:center;margin-top:60px'>"
|
||||
"✅ Saved. Restarting and joining <b>" + ss + "</b>…<br><br>"
|
||||
"✅ Saved. Restarting and joining <b>" + htmlEscape(ss) + "</b>…<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);
|
||||
|
||||
Reference in new issue
Block a user