Skip to content

Commit dcd3e07

Browse files
committed
Securing OTA update
- prevent settings change if not using private IP address or same subnet - prevent OTA from differnet subnet if PIN is not set - ability to revert firmware
1 parent f26733c commit dcd3e07

File tree

7 files changed

+66
-7
lines changed

7 files changed

+66
-7
lines changed

wled00/cfg.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) {
631631
CJSON(aOtaEnabled, ota[F("aota")]);
632632
#endif
633633
getStringFromJson(otaPass, pwd, 33); //normally not present due to security
634+
CJSON(otaSameSubnet, ota[F("same-subnet")]);
634635
}
635636

636637
#ifdef WLED_ENABLE_DMX
@@ -1127,6 +1128,7 @@ void serializeConfig(JsonObject root) {
11271128
#ifndef WLED_DISABLE_OTA
11281129
ota[F("aota")] = aOtaEnabled;
11291130
#endif
1131+
ota[F("same-subnet")] = otaSameSubnet;
11301132

11311133
#ifdef WLED_ENABLE_DMX
11321134
JsonObject dmx = root.createNestedObject("dmx");

wled00/data/settings_sec.htm

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ <h2>Security & Update setup</h2>
5757
<h3>Software Update</h3>
5858
<button type="button" onclick="U()">Manual OTA Update</button><br>
5959
<div id="aOTA">Enable ArduinoOTA: <input type="checkbox" name="AO"></div>
60+
Only allow update from same network/WiFi: <input type="checkbox" name="SU"><br>
61+
<i class="warn">&#9888; If you are using multiple VLANs (i.e. IoT or guest network) either set PIN or disable this option.<br>
62+
Disabling this option will make your device less secure.</i><br>
6063
<hr id="backup">
6164
<h3>Backup & Restore</h3>
6265
<div class="warn">&#9888; Restoring presets/configuration will OVERWRITE your current presets/configuration.<br>

wled00/data/update.htm

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,20 @@
33
<head>
44
<meta content='width=device-width' name='viewport'>
55
<title>WLED Update</title>
6+
<script src="common.js" async type="text/javascript"></script>
67
<script>
78
function B() { window.history.back(); }
8-
function U() { document.getElementById("uf").style.display="none";document.getElementById("msg").style.display="block"; }
9+
var cnfr = false;
10+
function cR() {
11+
if (!cnfr) {
12+
var bt = gId('rev');
13+
bt.style.color = "red";
14+
bt.innerText = "Revert!";
15+
cnfr = true;
16+
return;
17+
}
18+
window.open(getURL("/update?revert"),"_self");
19+
}
920
function GetV() {/*injected values here*/}
1021
</script>
1122
<style>
@@ -15,15 +26,17 @@
1526

1627
<body onload="GetV()">
1728
<h2>WLED Software Update</h2>
18-
<form method='POST' action='./update' id='uf' enctype='multipart/form-data' onsubmit="U()">
29+
<form method='POST' action='./update' id='upd' enctype='multipart/form-data' onsubmit="toggle('upd')">
1930
Installed version: <span class="sip">##VERSION##</span><br>
2031
Download the latest binary: <a href="https://github.com/wled-dev/WLED/releases" target="_blank"
2132
style="vertical-align: text-bottom; display: inline-flex;">
2233
<img src="https://img.shields.io/github/release/wled-dev/WLED.svg?style=flat-square"></a><br>
2334
<input type='file' name='update' required><br> <!--should have accept='.bin', but it prevents file upload from android app-->
2435
<button type="submit">Update!</button><br>
36+
<hr class="sml">
37+
<button id="rev" type="button" onclick="cR()">Revert update</button><br>
2538
<button type="button" onclick="B()">Back</button>
2639
</form>
27-
<div id="msg"><b>Updating...</b><br>Please do not close or refresh the page :)</div>
40+
<div id="Noupd" class="hide"><b>Updating...</b><br>Please do not close or refresh the page :)</div>
2841
</body>
2942
</html>

wled00/set.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
610610
aOtaEnabled = request->hasArg(F("AO"));
611611
#endif
612612
//createEditHandler(correctPIN && !otaLock);
613+
otaSameSubnet = request->hasArg(F("SU"));
613614
}
614615
}
615616

wled00/wled.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,7 @@ WLED_GLOBAL bool aOtaEnabled _INIT(true); // ArduinoOTA allows easy upda
590590
#else
591591
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
592592
#endif
593+
WLED_GLOBAL bool otaSameSubnet _INIT(true); // prevent OTA updates from other subnets (e.g. internet) if no PIN is set
593594
WLED_GLOBAL char settingsPIN[5] _INIT(WLED_PIN); // PIN for settings pages
594595
WLED_GLOBAL bool correctPIN _INIT(!strlen(settingsPIN));
595596
WLED_GLOBAL unsigned long lastEditTime _INIT(0);

wled00/wled_server.cpp

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ static const char s_redirecting[] PROGMEM = "Redirecting...";
1616
static const char s_content_enc[] PROGMEM = "Content-Encoding";
1717
static const char s_unlock_ota [] PROGMEM = "Please unlock OTA in security settings!";
1818
static const char s_unlock_cfg [] PROGMEM = "Please unlock settings using PIN code!";
19+
static const char s_rebooting [] PROGMEM = "Rebooting now...";
1920
static const char s_notimplemented[] PROGMEM = "Not implemented";
2021
static const char s_accessdenied[] PROGMEM = "Access Denied";
2122
static const char _common_js[] PROGMEM = "/common.js";
@@ -31,6 +32,22 @@ static bool isIp(const String &str) {
3132
return true;
3233
}
3334

35+
static bool inSubnet(const IPAddress &ip, const IPAddress &subnet, const IPAddress &mask) {
36+
return ((ip & mask) == (subnet & mask));
37+
}
38+
39+
static bool inSameSubnet(const IPAddress &client) {
40+
return inSubnet(client, Network.localIP(), Network.subnetMask());
41+
}
42+
43+
static bool inLocalSubnet(const IPAddress &client) {
44+
return inSubnet(client, IPAddress(10,0,0,0), IPAddress(255,0,0,0)) // 10.x.x.x
45+
|| inSubnet(client, IPAddress(192,168,0,0), IPAddress(255,255,0,0)) // 192.168.x.x
46+
|| inSubnet(client, IPAddress(172,16,0,0), IPAddress(255,240,0,0)) // 172.16.x.x
47+
|| (inSubnet(client, IPAddress(4,3,2,0), IPAddress(255,255,255,0)) && apActive) // WLED AP
48+
|| inSameSubnet(client); // same subnet as WLED device
49+
}
50+
3451
/*
3552
* Integrated HTTP web server page declarations
3653
*/
@@ -130,7 +147,7 @@ static String msgProcessor(const String& var)
130147
if (optt < 60) //redirect to settings after optionType seconds
131148
{
132149
messageBody += F("<script>setTimeout(RS,");
133-
messageBody +=String(optt*1000);
150+
messageBody += String(optt*1000);
134151
messageBody += F(")</script>");
135152
} else if (optt < 120) //redirect back after optionType-60 seconds, unused
136153
{
@@ -270,7 +287,7 @@ void initServer()
270287
});
271288

272289
server.on(F("/reset"), HTTP_GET, [](AsyncWebServerRequest *request){
273-
serveMessage(request, 200,F("Rebooting now..."),F("Please wait ~10 seconds..."),129);
290+
serveMessage(request, 200, FPSTR(s_rebooting), F("Please wait ~10 seconds."), 131);
274291
doReboot = true;
275292
});
276293

@@ -385,10 +402,16 @@ void initServer()
385402
if (Update.hasError()) {
386403
serveMessage(request, 500, F("Update failed!"), F("Please check your file and retry!"), 254);
387404
} else {
388-
serveMessage(request, 200, F("Update successful!"), F("Rebooting..."), 131);
405+
serveMessage(request, 200, F("Update successful!"), FPSTR(s_rebooting), 131);
389406
doReboot = true;
390407
}
391408
},[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool isFinal){
409+
IPAddress client = request->client()->remoteIP();
410+
if (((otaSameSubnet && !inSameSubnet(client)) && !strlen(settingsPIN)) || (!otaSameSubnet && !inLocalSubnet(client))) {
411+
DEBUG_PRINTLN(F("Attempted OTA update from different/non-local subnet!"));
412+
request->send(401, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_accessdenied));
413+
return;
414+
}
392415
if (!correctPIN || otaLock) return;
393416
if(!index){
394417
DEBUG_PRINTLN(F("OTA Update Start"));
@@ -573,6 +596,11 @@ void serveSettings(AsyncWebServerRequest* request, bool post) {
573596
}
574597

575598
if (post) { //settings/set POST request, saving
599+
IPAddress client = request->client()->remoteIP();
600+
if (!inLocalSubnet(client)) { // includes same subnet check
601+
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_redirecting), 123);
602+
return;
603+
}
576604
if (subPage != SUBPAGE_WIFI || !(wifiLock && otaLock)) handleSettingsSet(request, subPage);
577605

578606
char s[32];
@@ -624,7 +652,17 @@ void serveSettings(AsyncWebServerRequest* request, bool post) {
624652
case SUBPAGE_DMX : content = PAGE_settings_dmx; len = PAGE_settings_dmx_length; break;
625653
#endif
626654
case SUBPAGE_UM : content = PAGE_settings_um; len = PAGE_settings_um_length; break;
627-
case SUBPAGE_UPDATE : content = PAGE_update; len = PAGE_update_length; break;
655+
case SUBPAGE_UPDATE : content = PAGE_update; len = PAGE_update_length;
656+
if (request->hasArg(F("revert")) && inLocalSubnet(request->client()->remoteIP()) && Update.canRollBack()) {
657+
doReboot = Update.rollBack();
658+
if (doReboot) {
659+
serveMessage(request, 200, F("Reverted to previous version!"), FPSTR(s_rebooting), 133);
660+
} else {
661+
serveMessage(request, 500, F("Rollback failed!"), F("Please reboot and retry."), 254);
662+
}
663+
return;
664+
}
665+
break;
628666
#ifndef WLED_DISABLE_2D
629667
case SUBPAGE_2D : content = PAGE_settings_2D; len = PAGE_settings_2D_length; break;
630668
#endif

wled00/xml.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,7 @@ void getSettingsJS(byte subPage, Print& settingsScript)
591591
printSetFormCheckbox(settingsScript,PSTR("NO"),otaLock);
592592
printSetFormCheckbox(settingsScript,PSTR("OW"),wifiLock);
593593
printSetFormCheckbox(settingsScript,PSTR("AO"),aOtaEnabled);
594+
printSetFormCheckbox(settingsScript,PSTR("SU"),otaSameSubnet);
594595
char tmp_buf[128];
595596
snprintf_P(tmp_buf,sizeof(tmp_buf),PSTR("WLED %s (build %d)"),versionString,VERSION);
596597
printSetClassElementHTML(settingsScript,PSTR("sip"),0,tmp_buf);

0 commit comments

Comments
 (0)