Skip to content

Conversation

@blazoncek
Copy link
Contributor

@blazoncek blazoncek commented May 26, 2025

  • prevent settings change if not using private IP address or same subnet
  • prevent OTA from differnet/non-local subnet if PIN is not set
  • ability to revert firmware

Summary by CodeRabbit

  • New Features

    • Added an option to restrict OTA (Over-The-Air) updates to devices on the same network/subnet for enhanced security.
    • Introduced a checkbox in the Software Update settings to enable or disable this restriction, along with a warning for users with multiple VLANs.
    • Added a firmware rollback (revert update) feature with confirmation on the update page.
  • Bug Fixes

    • Unified reboot messages for consistency across the interface.
  • Refactor

    • Improved update form handling and confirmation logic for firmware rollback.
    • Enhanced access control for OTA and settings updates based on client network location.
  • Documentation

    • Updated interface with explanatory text for new security options.

- 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
@coderabbitai
Copy link
Contributor

coderabbitai bot commented May 26, 2025

Walkthrough

The changes introduce a new boolean configuration parameter, otaSameSubnet, to restrict OTA updates to clients within the same subnet. This parameter is added to the configuration handling, web UI, and access control logic. Additionally, the update page now includes a rollback feature with confirmation, and subnet-based access restrictions are enforced for sensitive operations.

Changes

Files/Paths Change Summary
wled00/cfg.cpp, wled00/wled.h, wled00/set.cpp, wled00/xml.cpp Added otaSameSubnet configuration parameter: declaration, serialization/deserialization, web form handling, and settings JavaScript output.
wled00/data/settings_sec.htm Added checkbox and warning for "Only allow update from same network/WiFi" in the Software Update section.
wled00/data/update.htm Refactored update form submission, added external script, implemented two-step revert update button, and updated element IDs/handlers.
wled00/wled_server.cpp Added subnet access control helpers; enforced subnet-based restrictions for OTA/settings POST; unified reboot messaging; added rollback logic for updates.

📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dcd3e07 and a147f41.

📒 Files selected for processing (2)
  • wled00/wled_server.cpp (7 hunks)
  • wled00/xml.cpp (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • wled00/xml.cpp
  • wled00/wled_server.cpp
⏰ Context from checks skipped due to timeout of 90000ms (19)
  • GitHub Check: wled_build / Build Environments (esp32dev_V4)
  • GitHub Check: wled_build / Build Environments (esp01_1m_full_compat)
  • GitHub Check: wled_build / Build Environments (esp32dev)
  • GitHub Check: wled_build / Build Environments (esp8266_2m_compat)
  • GitHub Check: wled_build / Build Environments (esp8266_2m_160)
  • GitHub Check: wled_build / Build Environments (esp01_1m_full)
  • GitHub Check: wled_build / Build Environments (nodemcuv2_160)
  • GitHub Check: wled_build / Build Environments (nodemcuv2)
  • GitHub Check: wled_build / Build Environments (esp8266_2m)
  • GitHub Check: wled_build / Build Environments (esp8266_2m_compat)
  • GitHub Check: wled_build / Build Environments (usermods)
  • GitHub Check: wled_build / Build Environments (nodemcuv2_compat)
  • GitHub Check: wled_build / Build Environments (esp01_1m_full_compat)
  • GitHub Check: wled_build / Build Environments (nodemcuv2_160)
  • GitHub Check: wled_build / Build Environments (esp01_1m_full_160)
  • GitHub Check: wled_build / Build Environments (esp8266_2m_160)
  • GitHub Check: wled_build / Build Environments (esp01_1m_full)
  • GitHub Check: wled_build / Build Environments (nodemcuv2)
  • GitHub Check: wled_build / Build Environments (esp8266_2m)
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f26733c and dcd3e07.

📒 Files selected for processing (7)
  • wled00/cfg.cpp (2 hunks)
  • wled00/data/settings_sec.htm (1 hunks)
  • wled00/data/update.htm (2 hunks)
  • wled00/set.cpp (1 hunks)
  • wled00/wled.h (1 hunks)
  • wled00/wled_server.cpp (7 hunks)
  • wled00/xml.cpp (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
wled00/wled_server.cpp (1)
wled00/fcn_declare.h (1)
  • serveMessage (611-611)
⏰ Context from checks skipped due to timeout of 90000ms (20)
  • GitHub Check: wled_build / Build Environments (lolin_s2_mini)
  • GitHub Check: wled_build / Build Environments (esp32_wrover)
  • GitHub Check: wled_build / Build Environments (usermods)
  • GitHub Check: wled_build / Build Environments (esp32s3_4M_qspi)
  • GitHub Check: wled_build / Build Environments (esp32_eth)
  • GitHub Check: wled_build / Build Environments (esp32c3dev)
  • GitHub Check: wled_build / Build Environments (esp32s3dev_16MB_opi)
  • GitHub Check: wled_build / Build Environments (esp32s3dev_8MB_opi)
  • GitHub Check: wled_build / Build Environments (esp01_1m_full_compat)
  • GitHub Check: wled_build / Build Environments (esp8266_2m_compat)
  • GitHub Check: wled_build / Build Environments (esp32dev_V4)
  • GitHub Check: wled_build / Build Environments (esp32dev)
  • GitHub Check: wled_build / Build Environments (nodemcuv2_compat)
  • GitHub Check: wled_build / Build Environments (esp01_1m_full_160)
  • GitHub Check: wled_build / Build Environments (esp01_1m_full)
  • GitHub Check: wled_build / Build Environments (esp8266_2m)
  • GitHub Check: wled_build / Build Environments (esp8266_2m_160)
  • GitHub Check: wled_build / Build Environments (nodemcuv2_160)
  • GitHub Check: wled_build / Build Environments (nodemcuv2)
  • GitHub Check: wled_build / Build Environments (usermods)
🔇 Additional comments (10)
wled00/wled.h (1)

593-593: Verify JSON serialization/deserialization of the new setting
Ensure that otaSameSubnet is correctly persisted in the configuration: it must be read from and written to the JSON config (in cfg.cpp) under the "same-subnet" key so user changes survive a reboot.

wled00/set.cpp (1)

613-613: Correct integration of otaSameSubnet in the settings handler
The new line properly captures the "SU" checkbox when password checks pass or OTA lock is disabled. This ensures the flag is updated from the HTTP request.

wled00/xml.cpp (1)

594-594: Expose otaSameSubnet in the security settings UI
Adding printSetFormCheckbox(settingsScript, PSTR("SU"), otaSameSubnet); correctly injects the checkbox state into the JS for the security page, making the new setting visible and editable.

wled00/data/settings_sec.htm (1)

60-62: LGTM! Good security enhancement with clear user guidance.

The new checkbox for subnet-based OTA restriction is well-integrated into the UI with an appropriate warning message that helps users understand the implications for multi-VLAN setups.

wled00/cfg.cpp (1)

634-634: Configuration handling is properly implemented.

The otaSameSubnet setting is correctly integrated into the configuration serialization/deserialization flow, following the established pattern for OTA settings.

Also applies to: 1131-1131

wled00/data/update.htm (2)

9-19: Well-implemented rollback feature with proper user confirmation.

The two-step confirmation mechanism for the rollback button prevents accidental triggering, with clear visual feedback (red color and text change) before executing the rollback.

Also applies to: 36-37


6-6:

Details

✅ Verification successful

Verify that the toggle() function is properly defined in common.js.

The form submission now relies on toggle('upd') but we need to ensure this function exists and properly handles the form submission and div visibility toggling.

Also applies to: 29-29, 40-40


🏁 Script executed:

#!/bin/bash
# Description: Verify that the toggle() function is defined in common.js

# Search for toggle function definition in common.js
fd -e js "common.js" --exec grep -l "function toggle" {} \;

# If found, show the implementation
fd -e js "common.js" --exec grep -A 10 "function toggle" {} \;

Length of output: 654


toggle() Verified in common.js

The toggle(el) function is defined in wled00/data/common.js and correctly toggles the hide class on both el and No + el elements:

  • File: wled00/data/common.js
    Implementation:
    function toggle(el) {
      gId(el).classList.toggle("hide");
      gId('No'+el).classList.toggle("hide");
    }

No further action required.

wled00/wled_server.cpp (3)

19-19: Good practice: Centralizing reboot messages.

Using a common flash string constant for reboot messages improves consistency and reduces flash memory usage.

Also applies to: 290-291, 405-405


599-604: Good security enhancement for settings protection.

Restricting settings POST requests to local subnets adds an important security layer. The implementation correctly uses the inLocalSubnet check which includes same subnet verification.


35-50:

Details

✅ Verification successful

Subnet checking implementation looks solid with one minor concern.

The helper functions correctly implement subnet membership checks using bitwise operations. However, the WLED AP subnet check on line 47 assumes the AP always uses 4.3.2.0/24, which might not be configurable.


🏁 Script executed:

#!/bin/bash
# Description: Verify if WLED AP network is always 4.3.2.0/24 or if it's configurable

# Search for AP IP configuration
rg -A 5 "4\.3\.2\.[0-9]" --type cpp

# Search for AP network configuration
rg -A 5 "apIP|AP_IP|ap.*ip" --type cpp -i

Length of output: 44499


AP IP is fixed at 4.3.2.1/24 by design; no changes needed

I verified that the AP address is hard-coded in the codebase (not loaded from user-config):

• wled00/wled.cpp:
WiFi.softAPConfig(IPAddress(4,3,2,1), IPAddress(4,3,2,1), IPAddress(255,255,255,0));
• wled00/cfg.cpp:
Default “ap.ip” is generated as [4,3,2,1], and legacy code to read it is commented out.

Since the AP subnet is intentionally fixed, the inLocalSubnet check remains accurate and no further action is required.

Comment on lines +409 to +414
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;
}
Copy link
Contributor

@coderabbitai coderabbitai bot May 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Security logic needs clarification.

The OTA update access control condition is complex and might have unintended behavior:

if (((otaSameSubnet && !inSameSubnet(client)) && !strlen(settingsPIN)) || (!otaSameSubnet && !inLocalSubnet(client)))

This logic means:

  • When otaSameSubnet is true: denies access if client is not in same subnet AND no PIN is set
  • When otaSameSubnet is false: denies access if client is not in local subnet

This could allow OTA updates from different subnets if a PIN is set, even when otaSameSubnet is true. Is this the intended behavior?

Consider simplifying the logic:

-if (((otaSameSubnet && !inSameSubnet(client)) && !strlen(settingsPIN)) || (!otaSameSubnet && !inLocalSubnet(client))) {
+if ((otaSameSubnet && !inSameSubnet(client)) || (!otaSameSubnet && !inLocalSubnet(client))) {
🤖 Prompt for AI Agents
In wled00/wled_server.cpp around lines 409 to 414, the OTA update access control
condition is overly complex and may unintentionally allow OTA updates from
different subnets when a PIN is set and otaSameSubnet is true. Simplify the
condition to clearly enforce that if otaSameSubnet is true, only clients in the
same subnet are allowed regardless of PIN, and if otaSameSubnet is false, only
clients in the local subnet are allowed. Adjust the logic to explicitly check
subnet membership first, then verify PIN presence if needed, ensuring no
unintended access is granted.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the intended behaviour. I admit it may not be the best.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blazoncek Thank you for clarifying that this was the intended behavior. I understand the design choice - allowing PIN-protected OTA updates from different subnets when otaSameSubnet is true does provide flexibility for users who need remote access.

The trade-off is between convenience (remote OTA with PIN) and security (strict subnet enforcement). If you ever decide to revisit this, you might consider:

  • Making the PIN requirement more explicit in the UI/documentation
  • Or separating the subnet restriction from PIN authentication entirely

But I appreciate you confirming the intentional design!


✏️ Learnings added
Learnt from: blazoncek
PR: wled/WLED#4700
File: wled00/wled_server.cpp:409-414
Timestamp: 2025-05-26T16:09:34.316Z
Learning: In wled00/wled_server.cpp, the OTA update access control logic intentionally allows OTA updates from different subnets when otaSameSubnet is true AND a PIN is set. This was a conscious design decision by blazoncek to provide flexibility for remote OTA updates with PIN protection, though they acknowledged it may not be the optimal security model.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Member

@willmmiles willmmiles left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic seems sound. Works for me in a simple test.

Comment on lines +44 to +46
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those darn old ESP32 platforms. :( In a modern system (ESP8266, ESP32 v4 or newer) we should be using client.isLocal() instead.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to test for this explicitly? Shouldn't this also pass inSameSubnet()?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arg, I forget that the AP might as well be a separate ethernet device as the client mode. Only real note here is that 'apActive' should probably be tested first; it'll save a couple of cycles in the average case, and be clearer that this line is different than the above.

Comment on lines +656 to +666
#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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bikeshedding: Could we implement this in the handler at wled_server.cpp:387 instead, like the update POST? It doesn't feel like it ought to be mixed in with the settings code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll look into it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, the issue is another set of checking and handling for PIN.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, 'revert' should have the same set of checks as a regular firmware upload. Best to put them in a common function I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried it, does not add anything really useful beyond duplicating handling of PIN from serveSettings() into callback function. serveSettings() does all checks.
Perhaps my view is skewed and you may have a different POV.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the heart of it, my concern is that serveSettings() doesn't just serve settings pages anymore -- it's becoming an octopus function that call paths are being forced to multiplex in to, then fan out of, just to get access to the authentication code. Triggering a revert isn't serving a settings page, so my $0.02 is that it doesn't belong in serveSettings().

Also it looks to me like authentication validation is replicated in the upload POST handler as well. This is a strong indication to me that the authentication validation code should be pulled out to its own function that can be called from different contexts, independent of serveSettings().

That said: I do not consider this a blocker on this patch -- it can be addressed later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I came up with:

  server.on(_update, HTTP_GET, [](AsyncWebServerRequest *request){
    if (otaLock) {
      serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_ota), 254);
    } else {
      #ifdef ARDUINO_ARCH_ESP32
      if (!correctPIN && strlen(settingsPIN) > 0) {
        handleStaticContent(request, "", 401, FPSTR(CONTENT_TYPE_HTML), PAGE_settings_pin, PAGE_settings_pin_length);
        return;
      }
      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
      serveSettings(request); // checks for "/update" in URL and also handles PIN
    }
  });

  server.on(_update, HTTP_POST, [](AsyncWebServerRequest *request){
    if (request->hasArg(F("PIN"))) {
      checkSettingsPIN(request->arg(F("PIN")).c_str());
      if (correctPIN) serveMessage(request, 200, F("PIN accepted"), FPSTR(s_redirecting), 1);
      else            serveMessage(request, 401, F("PIN rejected"), FPSTR(s_redirecting), 3);
      return;
    }
    // if OTA locked or too frequent PIN entry requests fail hard
    if (otaLock || !correctPIN) {
      serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_ota), 254);
      return;
    }
...

Which only moves PIN checking into GET handler (which is still included in serveSettings() for other purposes).

@netmindz
Copy link
Member

This would certainly help reduce the risk for those that (unwisely) use port forwarding to expose WLED to the internet.

I would not help anyone that uses a reverse proxy like NGINX to do similar

@netmindz
Copy link
Member

Given this is security related, should we be looking to include the the 0.15.1 bugfix release?

@blazoncek
Copy link
Contributor Author

Given this is security related, should we be looking to include the the 0.15.1 bugfix release?

Possibly yes. But I would put in a warning.

@netmindz
Copy link
Member

Given this is security related, should we be looking to include the the 0.15.1 bugfix release?

Possibly yes. But I would put in a warning.

Update to that version would be fine. It's just then trying to upgrade after this or downgrade that they might see issues. Good to flag in the release notes thought if we do include

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants