Skip to content

[fix] Unhandled exception when parsing invalid captive portal action url in status.js#1066

Open
Ontiomacer wants to merge 1 commit into
openwisp:masterfrom
Ontiomacer:status-url-parse-fix
Open

[fix] Unhandled exception when parsing invalid captive portal action url in status.js#1066
Ontiomacer wants to merge 1 commit into
openwisp:masterfrom
Ontiomacer:status-url-parse-fix

Conversation

@Ontiomacer
Copy link
Copy Markdown

Handle invalid or empty captivePortalLoginForm.action values safely by wrapping URL parsing in a try/catch block.
Also ensure safe destructuring of event.data to avoid runtime errors when the message payload is undefined.
This improves robustness of the postMessage handler without changing existing behavior.

Checklist

  • I have read the OpenWISP Contributing Guidelines.
  • I have manually tested the changes proposed in this pull request.
  • I have written new test cases for new code and/or updated existing tests for changes to existing code.
  • I have updated the documentation.

Reference to Existing Issue

Closes #1065.

Description of Changes

The handlePostMessage function parses captivePortalLoginForm.action using the URL constructor.

If the action value is empty or malformed, URL parsing may throw an exception. This change wraps the parsing logic in a try/catch block and validates the action before parsing.

Additionally, event.data is safely destructured to prevent runtime errors when the message payload is undefined.

These changes improve robustness of the message handler without altering existing behavior.

No functional changes are introduced.

Screenshot

N/A

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 20, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: efd68c5c-987e-4740-951a-d3ab4d09fd89

📥 Commits

Reviewing files that changed from the base of the PR and between 27b92b2 and f40dbbe.

📒 Files selected for processing (1)
  • client/components/status/status.js
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Tests and Coverage
  • GitHub Check: QA-Checks
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: nemesifier
Repo: openwisp/openwisp-wifi-login-pages PR: 0
File: :0-0
Timestamp: 2026-03-06T23:26:11.526Z
Learning: In client/components/status/status.js (openwisp-wifi-login-pages), catch blocks deliberately do NOT check `isComponentMounted` — console warnings from catch paths are considered harmless. Only real memory leaks (timers, event listeners) and "setState on unmounted" in success paths are fixed. The maintainer (nemesifier) explicitly wants to avoid over-engineering with defensive checks in every catch block.
Learnt from: nemesifier
Repo: openwisp/openwisp-wifi-login-pages PR: 0
File: :0-0
Timestamp: 2026-03-06T23:26:11.526Z
Learning: In client/components/status/status.js (openwisp-wifi-login-pages), `finalOperations()` has a single early return before setting up intervals/listeners to prevent the scenario where the component unmounts during async awaits. `safeSetState` is applied to success paths in `getUserRadiusSessions()`, `getUserRadiusUsage()`, `getPlansSuccessCallback()`, and `handlePostMessage()`.
Learnt from: nemesifier
Repo: openwisp/openwisp-wifi-login-pages PR: 0
File: :0-0
Timestamp: 2026-03-06T23:26:11.526Z
Learning: In client/components/status/status.js (openwisp-wifi-login-pages), the Status component uses `this.isComponentMounted` (set to `true` in the constructor, `false` in `componentWillUnmount`) together with a `safeSetState()` helper method that checks `isComponentMounted` before calling `setState()`. This is the preferred pattern to guard against "setState on unmounted component" warnings.
Learnt from: nemesifier
Repo: openwisp/openwisp-wifi-login-pages PR: 0
File: :0-0
Timestamp: 2026-03-06T23:26:11.526Z
Learning: In client/components/status/status.js (openwisp-wifi-login-pages), real memory leaks are fixed by calling `clearTimeout(this.usageRetryTimeoutId)` and `window.removeEventListener("message", ...)` in `componentWillUnmount()`. These were actual leaks — the retry timeout could fire and the message handler could run after unmount.
Learnt from: prathmeshkulkarni-coder
Repo: openwisp/openwisp-wifi-login-pages PR: 1063
File: client/components/status/status.test.js:2890-2916
Timestamp: 2026-03-18T17:34:17.347Z
Learning: In openwisp/openwisp-wifi-login-pages, JSDOM marks `window.location` as non-configurable (configurable: false) in the Jest/Enzyme test environment used by this project. Object.defineProperty on window.location throws a TypeError. Hacks like `delete window.location` cause state leaks across sequential tests. The maintainer (prathmeshkulkarni-coder) considers the window.location mocking approach infeasible and plans to migrate detection tests to RTL instead.
📚 Learning: 2026-03-06T23:26:11.526Z
Learnt from: nemesifier
Repo: openwisp/openwisp-wifi-login-pages PR: 0
File: :0-0
Timestamp: 2026-03-06T23:26:11.526Z
Learning: In client/components/status/status.js (openwisp-wifi-login-pages), `finalOperations()` has a single early return before setting up intervals/listeners to prevent the scenario where the component unmounts during async awaits. `safeSetState` is applied to success paths in `getUserRadiusSessions()`, `getUserRadiusUsage()`, `getPlansSuccessCallback()`, and `handlePostMessage()`.

Applied to files:

  • client/components/status/status.js
📚 Learning: 2026-03-06T23:26:11.526Z
Learnt from: nemesifier
Repo: openwisp/openwisp-wifi-login-pages PR: 0
File: :0-0
Timestamp: 2026-03-06T23:26:11.526Z
Learning: In client/components/status/status.js (openwisp-wifi-login-pages), catch blocks deliberately do NOT check `isComponentMounted` — console warnings from catch paths are considered harmless. Only real memory leaks (timers, event listeners) and "setState on unmounted" in success paths are fixed. The maintainer (nemesifier) explicitly wants to avoid over-engineering with defensive checks in every catch block.

Applied to files:

  • client/components/status/status.js
📚 Learning: 2026-03-06T23:26:11.526Z
Learnt from: nemesifier
Repo: openwisp/openwisp-wifi-login-pages PR: 0
File: :0-0
Timestamp: 2026-03-06T23:26:11.526Z
Learning: In client/components/status/status.js (openwisp-wifi-login-pages), the Status component uses `this.isComponentMounted` (set to `true` in the constructor, `false` in `componentWillUnmount`) together with a `safeSetState()` helper method that checks `isComponentMounted` before calling `setState()`. This is the preferred pattern to guard against "setState on unmounted component" warnings.

Applied to files:

  • client/components/status/status.js
📚 Learning: 2026-03-06T23:26:11.526Z
Learnt from: nemesifier
Repo: openwisp/openwisp-wifi-login-pages PR: 0
File: :0-0
Timestamp: 2026-03-06T23:26:11.526Z
Learning: In client/components/status/status.js (openwisp-wifi-login-pages), real memory leaks are fixed by calling `clearTimeout(this.usageRetryTimeoutId)` and `window.removeEventListener("message", ...)` in `componentWillUnmount()`. These were actual leaks — the retry timeout could fire and the message handler could run after unmount.

Applied to files:

  • client/components/status/status.js
🔇 Additional comments (3)
client/components/status/status.js (3)

748-748: LGTM! Safe destructuring prevents runtime errors.

The || {} fallback correctly handles cases where event.data is null or undefined, preventing destructuring failures.


753-764: LGTM! Properly addresses the unhandled exception issue.

The implementation correctly:

  1. Initializes actionOrigin to null as a safe default
  2. Guards URL parsing with both a truthy check (action?.trim()) and a try-catch
  3. Includes window.location.origin as trusted, which is necessary since the component itself posts messages from that origin (line 124: window.postMessage({type: "owlp-ready"}, window.location.origin))

When actionOrigin remains null (invalid/empty action), only same-origin messages are trusted—this is a safe, restrictive fallback.


766-774: LGTM! Guard condition correctly uses the new trust check.

The early return properly rejects messages that don't pass the isTrustedOrigin check while maintaining the existing validation for type and message requirements.


📝 Walkthrough

Walkthrough

The change hardens the handlePostMessage function in client/components/status/status.js. It defaults event.data to an empty object before destructuring, introduces actionOrigin initialized to null, sets it only when captivePortalLoginForm.action is non-empty, wraps new URL(...) in a try/catch to ignore invalid URLs, and computes isTrustedOrigin by comparing event.origin to actionOrigin or window.location.origin. The early-return guard now uses isTrustedOrigin.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Bug Fixes ❌ Error The PR fixes the bug by wrapping URL parsing in try-catch and validating action before use, but includes no regression tests despite the codebase having test infrastructure capabilities. Add a regression test that reproduces the bug by setting captivePortalLoginForm.action to an invalid URL value and verifying handlePostMessage does not throw an exception.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title uses the required [fix] prefix and clearly describes the main change: handling invalid captive portal action URLs in status.js, which directly addresses the linked issue #1065.
Description check ✅ Passed The description follows the template structure with completed checklist items, references issue #1065, and provides detailed explanation of changes. However, test cases and documentation updates are not marked as completed.
Linked Issues check ✅ Passed The PR directly addresses all requirements from issue #1065: wrapping URL parsing in try/catch to prevent unhandled exceptions and safely destructuring event.data, which prevents runtime errors when the action is invalid or empty.
Out of Scope Changes check ✅ Passed All changes are scoped to the handlePostMessage function in status.js and directly address the issue without introducing unrelated modifications to other areas.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@Ontiomacer Ontiomacer changed the title [fix] Prevent crash when parsing invalid captive portal action url [fix] Unhandled exception when parsing invalid captive portal action url in status.js Mar 20, 2026
@openwisp-companion
Copy link
Copy Markdown

Test Failures and Code Style Issues

Hello @Ontiomacer,
(Analysis for commit 27b92b2)

  • Test Failure: The client/components/status/status.test.js test suite failed due to Jest worker exceptions. This is likely caused by the ReferenceError: trustedOrigin is not defined error occurring in client/components/status/status.js.

  • Fix: Examine client/components/status/status.js around line 13130 and ensure trustedOrigin is properly defined or handled before being used.

  • Code Style/QA: Prettier found code style issues in client/components/status/status.js.

  • Fix: Run openwisp-qa-format to automatically fix these issues.

Copy link
Copy Markdown

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
client/components/status/status.js (1)

753-775: ⚠️ Potential issue | 🔴 Critical

Critical bug: Undefined variable trustedOrigin referenced on line 768.

The code declares isTrustedOrigin on line 763 but references trustedOrigin on line 768. Since trustedOrigin is undefined, the condition !trustedOrigin will always evaluate to true, causing the function to return early on every postMessage event. This completely breaks the handler's functionality.

Additionally, the indentation of lines 753-765 is inconsistent with the rest of the method.

🐛 Proposed fix
-let actionOrigin = null;
-
-try {
-  if (captivePortalLoginForm.action?.trim()) {
-    actionOrigin = new URL(captivePortalLoginForm.action).origin;
-  }
-} catch {
-  // invalid URL, ignore
-}
-
-const isTrustedOrigin =
-  event.origin === actionOrigin ||
-  event.origin === window.location.origin;
+    let actionOrigin = null;
+
+    try {
+      if (captivePortalLoginForm.action?.trim()) {
+        actionOrigin = new URL(captivePortalLoginForm.action).origin;
+      }
+    } catch {
+      // invalid URL, ignore
+    }
+
+    const isTrustedOrigin =
+      event.origin === actionOrigin ||
+      event.origin === window.location.origin;

     if (
-      !trustedOrigin ||
+      !isTrustedOrigin ||
       !type ||
       // internet-mode will not contain message, but message
       // is required for authError and authMessage type
       (!message && type !== "internet-mode")
     ) {
       return;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/components/status/status.js` around lines 753 - 775, The handler
mistakenly references an undefined variable `trustedOrigin` instead of the
computed `isTrustedOrigin`, causing early returns; change the condition to use
`isTrustedOrigin` (i.e., replace `!trustedOrigin` with `!isTrustedOrigin`) and
keep the rest of the condition as-is (checking `type` and `message`), and also
fix the indentation of the block that computes `actionOrigin`/`isTrustedOrigin`
(the code around `captivePortalLoginForm`, `actionOrigin`, and
`isTrustedOrigin`) to match the surrounding method style so it's consistently
indented.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@client/components/status/status.js`:
- Around line 753-775: The handler mistakenly references an undefined variable
`trustedOrigin` instead of the computed `isTrustedOrigin`, causing early
returns; change the condition to use `isTrustedOrigin` (i.e., replace
`!trustedOrigin` with `!isTrustedOrigin`) and keep the rest of the condition
as-is (checking `type` and `message`), and also fix the indentation of the block
that computes `actionOrigin`/`isTrustedOrigin` (the code around
`captivePortalLoginForm`, `actionOrigin`, and `isTrustedOrigin`) to match the
surrounding method style so it's consistently indented.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 82dea135-bec7-4e18-ae1b-281a9e818c1e

📥 Commits

Reviewing files that changed from the base of the PR and between 5eef317 and 27b92b2.

📒 Files selected for processing (1)
  • client/components/status/status.js
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Tests and Coverage
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: nemesifier
Repo: openwisp/openwisp-wifi-login-pages PR: 0
File: :0-0
Timestamp: 2026-03-06T23:26:11.526Z
Learning: In client/components/status/status.js (openwisp-wifi-login-pages), catch blocks deliberately do NOT check `isComponentMounted` — console warnings from catch paths are considered harmless. Only real memory leaks (timers, event listeners) and "setState on unmounted" in success paths are fixed. The maintainer (nemesifier) explicitly wants to avoid over-engineering with defensive checks in every catch block.
Learnt from: nemesifier
Repo: openwisp/openwisp-wifi-login-pages PR: 0
File: :0-0
Timestamp: 2026-03-06T23:26:11.526Z
Learning: In client/components/status/status.js (openwisp-wifi-login-pages), `finalOperations()` has a single early return before setting up intervals/listeners to prevent the scenario where the component unmounts during async awaits. `safeSetState` is applied to success paths in `getUserRadiusSessions()`, `getUserRadiusUsage()`, `getPlansSuccessCallback()`, and `handlePostMessage()`.
Learnt from: nemesifier
Repo: openwisp/openwisp-wifi-login-pages PR: 0
File: :0-0
Timestamp: 2026-03-06T23:26:11.526Z
Learning: In client/components/status/status.js (openwisp-wifi-login-pages), real memory leaks are fixed by calling `clearTimeout(this.usageRetryTimeoutId)` and `window.removeEventListener("message", ...)` in `componentWillUnmount()`. These were actual leaks — the retry timeout could fire and the message handler could run after unmount.
Learnt from: prathmeshkulkarni-coder
Repo: openwisp/openwisp-wifi-login-pages PR: 1063
File: client/components/status/status.test.js:2890-2916
Timestamp: 2026-03-18T17:34:17.347Z
Learning: In openwisp/openwisp-wifi-login-pages, JSDOM marks `window.location` as non-configurable (configurable: false) in the Jest/Enzyme test environment used by this project. Object.defineProperty on window.location throws a TypeError. Hacks like `delete window.location` cause state leaks across sequential tests. The maintainer (prathmeshkulkarni-coder) considers the window.location mocking approach infeasible and plans to migrate detection tests to RTL instead.
Learnt from: nemesifier
Repo: openwisp/openwisp-wifi-login-pages PR: 0
File: :0-0
Timestamp: 2026-03-06T23:26:11.526Z
Learning: In client/components/status/status.js (openwisp-wifi-login-pages), the Status component uses `this.isComponentMounted` (set to `true` in the constructor, `false` in `componentWillUnmount`) together with a `safeSetState()` helper method that checks `isComponentMounted` before calling `setState()`. This is the preferred pattern to guard against "setState on unmounted component" warnings.
📚 Learning: 2026-03-06T23:26:11.526Z
Learnt from: nemesifier
Repo: openwisp/openwisp-wifi-login-pages PR: 0
File: :0-0
Timestamp: 2026-03-06T23:26:11.526Z
Learning: In client/components/status/status.js (openwisp-wifi-login-pages), `finalOperations()` has a single early return before setting up intervals/listeners to prevent the scenario where the component unmounts during async awaits. `safeSetState` is applied to success paths in `getUserRadiusSessions()`, `getUserRadiusUsage()`, `getPlansSuccessCallback()`, and `handlePostMessage()`.

Applied to files:

  • client/components/status/status.js
📚 Learning: 2026-03-06T23:26:11.526Z
Learnt from: nemesifier
Repo: openwisp/openwisp-wifi-login-pages PR: 0
File: :0-0
Timestamp: 2026-03-06T23:26:11.526Z
Learning: In client/components/status/status.js (openwisp-wifi-login-pages), catch blocks deliberately do NOT check `isComponentMounted` — console warnings from catch paths are considered harmless. Only real memory leaks (timers, event listeners) and "setState on unmounted" in success paths are fixed. The maintainer (nemesifier) explicitly wants to avoid over-engineering with defensive checks in every catch block.

Applied to files:

  • client/components/status/status.js
📚 Learning: 2026-03-06T23:26:11.526Z
Learnt from: nemesifier
Repo: openwisp/openwisp-wifi-login-pages PR: 0
File: :0-0
Timestamp: 2026-03-06T23:26:11.526Z
Learning: In client/components/status/status.js (openwisp-wifi-login-pages), the Status component uses `this.isComponentMounted` (set to `true` in the constructor, `false` in `componentWillUnmount`) together with a `safeSetState()` helper method that checks `isComponentMounted` before calling `setState()`. This is the preferred pattern to guard against "setState on unmounted component" warnings.

Applied to files:

  • client/components/status/status.js
🔇 Additional comments (1)
client/components/status/status.js (1)

748-748: LGTM!

Good defensive coding - defaulting event.data to {} prevents runtime errors when the message payload is undefined.

Handle invalid or empty captivePortalLoginForm.action values safely by wrapping URL parsing in a try/catch block.

Also ensure safe destructuring of event.data to avoid runtime errors when the message payload is undefined.

This improves robustness of the postMessage handler without changing existing behavior.
@Ontiomacer Ontiomacer force-pushed the status-url-parse-fix branch from 27b92b2 to f40dbbe Compare March 20, 2026 17:07
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.

[bug] Unhandled exception when parsing invalid captive portal action url in status.js

1 participant