Merge pull request #4700 from wled/secure-ota

Securing OTA update
This commit is contained in:
Blaž Kristan 2025-06-20 20:05:11 +02:00 committed by GitHub
commit 0f00c95aba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 71 additions and 7 deletions

View File

@ -740,6 +740,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
CJSON(aOtaEnabled, ota[F("aota")]); CJSON(aOtaEnabled, ota[F("aota")]);
#endif #endif
getStringFromJson(otaPass, pwd, 33); //normally not present due to security getStringFromJson(otaPass, pwd, 33); //normally not present due to security
CJSON(otaSameSubnet, ota[F("same-subnet")]);
} }
#ifdef WLED_ENABLE_DMX #ifdef WLED_ENABLE_DMX
@ -1218,6 +1219,7 @@ void serializeConfig(JsonObject root) {
#ifndef WLED_DISABLE_OTA #ifndef WLED_DISABLE_OTA
ota[F("aota")] = aOtaEnabled; ota[F("aota")] = aOtaEnabled;
#endif #endif
ota[F("same-subnet")] = otaSameSubnet;
#ifdef WLED_ENABLE_DMX #ifdef WLED_ENABLE_DMX
JsonObject dmx = root.createNestedObject("dmx"); JsonObject dmx = root.createNestedObject("dmx");

View File

@ -57,6 +57,9 @@
<h3>Software Update</h3> <h3>Software Update</h3>
<button type="button" onclick="U()">Manual OTA Update</button><br> <button type="button" onclick="U()">Manual OTA Update</button><br>
<div id="aOTA">Enable ArduinoOTA: <input type="checkbox" name="AO"></div> <div id="aOTA">Enable ArduinoOTA: <input type="checkbox" name="AO"></div>
Only allow update from same network/WiFi: <input type="checkbox" name="SU"><br>
<i class="warn">&#9888; If you are using multiple VLANs (i.e. IoT or guest network) either set PIN or disable this option.<br>
Disabling this option will make your device less secure.</i><br>
<hr id="backup"> <hr id="backup">
<h3>Backup & Restore</h3> <h3>Backup & Restore</h3>
<div class="warn">&#9888; Restoring presets/configuration will OVERWRITE your current presets/configuration.<br> <div class="warn">&#9888; Restoring presets/configuration will OVERWRITE your current presets/configuration.<br>

View File

@ -3,9 +3,20 @@
<head> <head>
<meta content='width=device-width' name='viewport'> <meta content='width=device-width' name='viewport'>
<title>WLED Update</title> <title>WLED Update</title>
<script src="common.js" async type="text/javascript"></script>
<script> <script>
function B() { window.history.back(); } function B() { window.history.back(); }
function U() { document.getElementById("uf").style.display="none";document.getElementById("msg").style.display="block"; } var cnfr = false;
function cR() {
if (!cnfr) {
var bt = gId('rev');
bt.style.color = "red";
bt.innerText = "Revert!";
cnfr = true;
return;
}
window.open(getURL("/update?revert"),"_self");
}
function GetV() {/*injected values here*/} function GetV() {/*injected values here*/}
</script> </script>
<style> <style>
@ -15,15 +26,17 @@
<body onload="GetV()"> <body onload="GetV()">
<h2>WLED Software Update</h2> <h2>WLED Software Update</h2>
<form method='POST' action='./update' id='uf' enctype='multipart/form-data' onsubmit="U()"> <form method='POST' action='./update' id='upd' enctype='multipart/form-data' onsubmit="toggle('upd')">
Installed version: <span class="sip">##VERSION##</span><br> Installed version: <span class="sip">##VERSION##</span><br>
Download the latest binary: <a href="https://github.com/wled-dev/WLED/releases" target="_blank" Download the latest binary: <a href="https://github.com/wled-dev/WLED/releases" target="_blank"
style="vertical-align: text-bottom; display: inline-flex;"> style="vertical-align: text-bottom; display: inline-flex;">
<img src="https://img.shields.io/github/release/wled-dev/WLED.svg?style=flat-square"></a><br> <img src="https://img.shields.io/github/release/wled-dev/WLED.svg?style=flat-square"></a><br>
<input type='file' name='update' required><br> <!--should have accept='.bin', but it prevents file upload from android app--> <input type='file' name='update' required><br> <!--should have accept='.bin', but it prevents file upload from android app-->
<button type="submit">Update!</button><br> <button type="submit">Update!</button><br>
<hr class="sml">
<button id="rev" type="button" onclick="cR()">Revert update</button><br>
<button type="button" onclick="B()">Back</button> <button type="button" onclick="B()">Back</button>
</form> </form>
<div id="msg"><b>Updating...</b><br>Please do not close or refresh the page :)</div> <div id="Noupd" class="hide"><b>Updating...</b><br>Please do not close or refresh the page :)</div>
</body> </body>
</html> </html>

View File

@ -612,6 +612,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
aOtaEnabled = request->hasArg(F("AO")); aOtaEnabled = request->hasArg(F("AO"));
#endif #endif
//createEditHandler(correctPIN && !otaLock); //createEditHandler(correctPIN && !otaLock);
otaSameSubnet = request->hasArg(F("SU"));
} }
} }

View File

@ -593,6 +593,7 @@ WLED_GLOBAL bool aOtaEnabled _INIT(true); // ArduinoOTA allows easy upda
#else #else
WLED_GLOBAL bool aOtaEnabled _INIT(false); // ArduinoOTA allows easy updates directly from the IDE. Careful, it does not auto-disable when OTA lock is on WLED_GLOBAL bool aOtaEnabled _INIT(false); // ArduinoOTA allows easy updates directly from the IDE. Careful, it does not auto-disable when OTA lock is on
#endif #endif
WLED_GLOBAL bool otaSameSubnet _INIT(true); // prevent OTA updates from other subnets (e.g. internet) if no PIN is set
WLED_GLOBAL char settingsPIN[5] _INIT(WLED_PIN); // PIN for settings pages WLED_GLOBAL char settingsPIN[5] _INIT(WLED_PIN); // PIN for settings pages
WLED_GLOBAL bool correctPIN _INIT(!strlen(settingsPIN)); WLED_GLOBAL bool correctPIN _INIT(!strlen(settingsPIN));
WLED_GLOBAL unsigned long lastEditTime _INIT(0); WLED_GLOBAL unsigned long lastEditTime _INIT(0);

View File

@ -16,6 +16,7 @@ static const char s_redirecting[] PROGMEM = "Redirecting...";
static const char s_content_enc[] PROGMEM = "Content-Encoding"; static const char s_content_enc[] PROGMEM = "Content-Encoding";
static const char s_unlock_ota [] PROGMEM = "Please unlock OTA in security settings!"; static const char s_unlock_ota [] PROGMEM = "Please unlock OTA in security settings!";
static const char s_unlock_cfg [] PROGMEM = "Please unlock settings using PIN code!"; static const char s_unlock_cfg [] PROGMEM = "Please unlock settings using PIN code!";
static const char s_rebooting [] PROGMEM = "Rebooting now...";
static const char s_notimplemented[] PROGMEM = "Not implemented"; static const char s_notimplemented[] PROGMEM = "Not implemented";
static const char s_accessdenied[] PROGMEM = "Access Denied"; static const char s_accessdenied[] PROGMEM = "Access Denied";
static const char _common_js[] PROGMEM = "/common.js"; static const char _common_js[] PROGMEM = "/common.js";
@ -31,6 +32,22 @@ static bool isIp(const String &str) {
return true; return true;
} }
static bool inSubnet(const IPAddress &ip, const IPAddress &subnet, const IPAddress &mask) {
return (((uint32_t)ip & (uint32_t)mask) == ((uint32_t)subnet & (uint32_t)mask));
}
static bool inSameSubnet(const IPAddress &client) {
return inSubnet(client, Network.localIP(), Network.subnetMask());
}
static bool inLocalSubnet(const IPAddress &client) {
return inSubnet(client, IPAddress(10,0,0,0), IPAddress(255,0,0,0)) // 10.x.x.x
|| inSubnet(client, IPAddress(192,168,0,0), IPAddress(255,255,0,0)) // 192.168.x.x
|| inSubnet(client, IPAddress(172,16,0,0), IPAddress(255,240,0,0)) // 172.16.x.x
|| (inSubnet(client, IPAddress(4,3,2,0), IPAddress(255,255,255,0)) && apActive) // WLED AP
|| inSameSubnet(client); // same subnet as WLED device
}
/* /*
* Integrated HTTP web server page declarations * Integrated HTTP web server page declarations
*/ */
@ -130,7 +147,7 @@ static String msgProcessor(const String& var)
if (optt < 60) //redirect to settings after optionType seconds if (optt < 60) //redirect to settings after optionType seconds
{ {
messageBody += F("<script>setTimeout(RS,"); messageBody += F("<script>setTimeout(RS,");
messageBody +=String(optt*1000); messageBody += String(optt*1000);
messageBody += F(")</script>"); messageBody += F(")</script>");
} else if (optt < 120) //redirect back after optionType-60 seconds, unused } else if (optt < 120) //redirect back after optionType-60 seconds, unused
{ {
@ -270,7 +287,7 @@ void initServer()
}); });
server.on(F("/reset"), HTTP_GET, [](AsyncWebServerRequest *request){ server.on(F("/reset"), HTTP_GET, [](AsyncWebServerRequest *request){
serveMessage(request, 200,F("Rebooting now..."),F("Please wait ~10 seconds..."),129); serveMessage(request, 200, FPSTR(s_rebooting), F("Please wait ~10 seconds."), 131);
doReboot = true; doReboot = true;
}); });
@ -385,10 +402,16 @@ void initServer()
if (Update.hasError()) { if (Update.hasError()) {
serveMessage(request, 500, F("Update failed!"), F("Please check your file and retry!"), 254); serveMessage(request, 500, F("Update failed!"), F("Please check your file and retry!"), 254);
} else { } else {
serveMessage(request, 200, F("Update successful!"), F("Rebooting..."), 131); serveMessage(request, 200, F("Update successful!"), FPSTR(s_rebooting), 131);
doReboot = true; doReboot = true;
} }
},[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){ },[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){
IPAddress client = request->client()->remoteIP();
if (((otaSameSubnet && !inSameSubnet(client)) && !strlen(settingsPIN)) || (!otaSameSubnet && !inLocalSubnet(client))) {
DEBUG_PRINTLN(F("Attempted OTA update from different/non-local subnet!"));
request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_accessdenied));
return;
}
if (!correctPIN || otaLock) return; if (!correctPIN || otaLock) return;
if(!index){ if(!index){
DEBUG_PRINTLN(F("OTA Update Start")); DEBUG_PRINTLN(F("OTA Update Start"));
@ -573,6 +596,11 @@ void serveSettings(AsyncWebServerRequest* request, bool post) {
} }
if (post) { //settings/set POST request, saving if (post) { //settings/set POST request, saving
IPAddress client = request->client()->remoteIP();
if (!inLocalSubnet(client)) { // includes same subnet check
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_redirecting), 123);
return;
}
if (subPage != SUBPAGE_WIFI || !(wifiLock && otaLock)) handleSettingsSet(request, subPage); if (subPage != SUBPAGE_WIFI || !(wifiLock && otaLock)) handleSettingsSet(request, subPage);
char s[32]; char s[32];
@ -624,7 +652,19 @@ void serveSettings(AsyncWebServerRequest* request, bool post) {
case SUBPAGE_DMX : content = PAGE_settings_dmx; len = PAGE_settings_dmx_length; break; case SUBPAGE_DMX : content = PAGE_settings_dmx; len = PAGE_settings_dmx_length; break;
#endif #endif
case SUBPAGE_UM : content = PAGE_settings_um; len = PAGE_settings_um_length; break; case SUBPAGE_UM : content = PAGE_settings_um; len = PAGE_settings_um_length; break;
case SUBPAGE_UPDATE : content = PAGE_update; len = PAGE_update_length; break; case SUBPAGE_UPDATE : content = PAGE_update; len = PAGE_update_length;
#ifdef ARDUINO_ARCH_ESP32
if (request->hasArg(F("revert")) && inLocalSubnet(request->client()->remoteIP()) && Update.canRollBack()) {
doReboot = Update.rollBack();
if (doReboot) {
serveMessage(request, 200, F("Reverted to previous version!"), FPSTR(s_rebooting), 133);
} else {
serveMessage(request, 500, F("Rollback failed!"), F("Please reboot and retry."), 254);
}
return;
}
#endif
break;
#ifndef WLED_DISABLE_2D #ifndef WLED_DISABLE_2D
case SUBPAGE_2D : content = PAGE_settings_2D; len = PAGE_settings_2D_length; break; case SUBPAGE_2D : content = PAGE_settings_2D; len = PAGE_settings_2D_length; break;
#endif #endif

View File

@ -591,6 +591,7 @@ void getSettingsJS(byte subPage, Print& settingsScript)
printSetFormCheckbox(settingsScript,PSTR("NO"),otaLock); printSetFormCheckbox(settingsScript,PSTR("NO"),otaLock);
printSetFormCheckbox(settingsScript,PSTR("OW"),wifiLock); printSetFormCheckbox(settingsScript,PSTR("OW"),wifiLock);
printSetFormCheckbox(settingsScript,PSTR("AO"),aOtaEnabled); printSetFormCheckbox(settingsScript,PSTR("AO"),aOtaEnabled);
printSetFormCheckbox(settingsScript,PSTR("SU"),otaSameSubnet);
char tmp_buf[128]; char tmp_buf[128];
snprintf_P(tmp_buf,sizeof(tmp_buf),PSTR("WLED %s (build %d)"),versionString,VERSION); snprintf_P(tmp_buf,sizeof(tmp_buf),PSTR("WLED %s (build %d)"),versionString,VERSION);
printSetClassElementHTML(settingsScript,PSTR("sip"),0,tmp_buf); printSetClassElementHTML(settingsScript,PSTR("sip"),0,tmp_buf);
@ -662,6 +663,9 @@ void getSettingsJS(byte subPage, Print& settingsScript)
VERSION); VERSION);
printSetClassElementHTML(settingsScript,PSTR("sip"),0,tmp_buf); printSetClassElementHTML(settingsScript,PSTR("sip"),0,tmp_buf);
#ifndef ARDUINO_ARCH_ESP32
settingsScript.print(F("toggle('rev');")); // hide revert button on ESP8266
#endif
} }
if (subPage == SUBPAGE_2D) // 2D matrices if (subPage == SUBPAGE_2D) // 2D matrices