Submit #896583: dgtlmoon changedetection.io 0.55.8 CWE-918 (SSRF)info

Titledgtlmoon changedetection.io 0.55.8 CWE-918 (SSRF)
Description# Server-Side Request Forgery via Goto URL Browser Step (CWE-918) **BUG_Author:** herantong **Affected Version:** changedetection.io ≤ 0.55.8 **Vendor:** [changedetection.io GitHub Repository](https://github.com/dgtlmoon/changedetection.io) **Software:** [changedetection.io](https://github.com/dgtlmoon/changedetection.io) **Vulnerability Files:** - `changedetectionio/browser_steps/browser_steps.py` - `changedetectionio/blueprint/browser_steps/__init__.py` - `changedetectionio/content_fetchers/base.py` --- ## Description ### 1. Server-Side Request Forgery via Unsanitized Goto URL The `action_goto_url` method accepts a user-controlled `value` parameter and passes it directly to Playwright's `page.goto()` without any URL validation. This enables browser-based Server-Side Request Forgery (SSRF) attacks, allowing access to internal URLs, localhost services, and cloud metadata endpoints (CWE-918). ### 2. Vulnerable Code Location The vulnerability is at `changedetectionio/browser_steps/browser_steps.py:134-142`: ```python # changedetectionio/browser_steps/browser_steps.py:134-142 async def action_goto_url(self, selector=None, value=None): if not value: logger.warning("No URL provided for goto_url action") return None now = time.time() response = await self.page.goto(value, timeout=0, wait_until='load') logger.debug(f"Time to goto URL {time.time()-now:.2f}s") return response ``` The `value` parameter is passed directly to `self.page.goto()` without any validation, sanitization, or whitelist check. The `page` object is a Playwright page running in a server-side headless browser context. ### 3. Interactive Attack Path via HTTP Endpoint At `changedetectionio/blueprint/browser_steps/__init__.py:352-391`: ```python @browser_steps_blueprint.route("/browsersteps_update", methods=['POST']) @login_optionally_required def browsersteps_ui_update(): step_operation = request.form.get('operation') step_selector = request.form.get('selector') step_optional_value = request.form.get('optional_value') run_async_in_browser_loop( browsersteps_sessions[browsersteps_session_id]['browserstepper'].call_action( action_name=step_operation, selector=step_selector, optional_value=step_optional_value ) ) ``` `step_optional_value` comes directly from `request.form.get('optional_value')`, a user-controlled HTTP POST form field. When `step_operation` is `"Goto URL"`, the value flows directly to `action_goto_url` as the `value` parameter. ### 4. Automated Attack Path via Watch Configuration At `changedetectionio/content_fetchers/base.py:191-219`: ```python async def iterate_browser_steps(self, start_url=None): for step in valid_steps: optional_value = step['optional_value'] selector = step['selector'] await getattr(interface, "call_action")(action_name=step['operation'], selector=selector, optional_value=optional_value) ``` `optional_value` comes from `step['optional_value']`, stored in a watch's `browser_steps` configuration. These steps are configured by the user via the UI or API and executed during subsequent automated fetches. ### 5. Existing SSRF Protections Do Not Cover Browser Steps The project provides URL validation utilities in `changedetectionio/validate_url.py` (`is_safe_valid_url`, `is_url_private_or_parser_confused`, `is_private_hostname`). These are used in the following contexts: - Watch URL validation on creation (`changedetectionio/model/Watch.py:279, :300`) - IANA private IP blocking during fetching (`changedetectionio/processors/base.py:110`) - Request fetcher redirect validation (`changedetectionio/content_fetchers/requests.py:93, :116`) - LLM API base URL validation (`changedetectionio/validate_url.py:136`) However, none of these validators are applied to the `optional_value` of the "Goto URL" browser step, either in the interactive `/browsersteps_update` endpoint or the automated `iterate_browser_steps` fetch path. The `validate_iana_url()` check at `processors/base.py:110` validates only `self.watch.link`, not individual browser step values. ### 6. Weakened Browser Security Controls Both browser context creation points set `bypass_csp=True` and `ignore_https_errors=True`: `changedetectionio/browser_steps/browser_steps.py:359-368`: ```python self.context = await self.playwright_browser.new_context( accept_downloads=False, bypass_csp=True, extra_http_headers=self.headers, ignore_https_errors=True, proxy=proxy, ) ``` `changedetectionio/content_fetchers/playwright.py:285-293`: ```python context = await browser.new_context( accept_downloads=False, bypass_csp=True, extra_http_headers=request_headers, ignore_https_errors=True, proxy=self.proxy, ) ``` These settings allow the browser to load content from sites with CSP restrictions and invalid TLS certificates, amplifying SSRF impact. ### 7. Authentication Context ```python @browser_steps_blueprint.route("/browsersteps_update", methods=['POST']) @login_optionally_required ``` The `login_optionally_required` decorator only enforces authentication when the application has a password configured. If no password is set, the endpoint is accessible without authentication. Even when authentication is required, this remains an authenticated SSRF vulnerability. --- ## Proof of Concept ### 1. Access Internal Services via Interactive Endpoint ``` POST http://<target-host>/browsersteps_update Content-Type: application/x-www-form-urlencoded operation=Goto URL&optional_value=http://127.0.0.1:8080/admin ``` ### 2. Access Cloud Metadata Endpoint ``` POST http://<target-host>/browsersteps_update Content-Type: application/x-www-form-urlencoded operation=Goto URL&optional_value=http://x.x.x.x/latest/meta-data/ ``` ### 3. Persistent SSRF via Watch Configuration Configure a watch with a browser step containing an internal URL. On each scheduled fetch, the server-side browser navigates to the internal target, enabling persistent information gathering or internal service interaction. ### 4. Attack Flow 1. An attacker sends a POST request to `/browsersteps_update` with `operation=Goto URL` and `optional_value` set to an internal URL. 2. The unsanitized URL reaches `action_goto_url` and is passed to `page.goto()`. 3. The server-side Playwright browser navigates to the internal URL. 4. The attacker can access internal services, cloud metadata endpoints, or localhost services. 5. With `bypass_csp=True` and `ignore_https_errors=True`, CSP and TLS restrictions are disabled, maximizing reachable targets. --- ## Root Cause Analysis | Question | Answer | |---|---| | Does user-controlled input influence the URL/host of a server-side request? | Yes — in the interactive path, `request.form.get('optional_value')` flows directly to `page.goto()`. In the automated path, user-configured `step['optional_value']` also reaches `page.goto()`. | | Is the target hostname/URL validated against a whitelist? | No — no whitelist validation exists for browser step values. | | Is the resolved IP checked against internal/private ranges? | No — `validate_iana_url()` in `processors/base.py` applies only to `self.watch.link`, not browser step values. | | Is only the path/query portion user-controlled with a hardcoded host? | No — the entire URL including protocol and host is user-controlled. | | Does the project already have SSRF defenses that could be reused? | Yes — `is_private_hostname()` and `is_url_private_or_parser_confused()` exist in `validate_url.py` but are not applied to browser steps. | | Are browser security controls hardened? | No — `bypass_csp=True` and `ignore_https_errors=True` amplify SSRF impact. | | Is the code in a test, demo, or dead-code context? | No — it is live production code reachable via HTTP endpoint and background worker. | **Verdict: Confirmed vulnerability (CWE-918 — Server-Side Request Forgery).** --- ## Fix Recommendations 1. **Add URL Validation Before `page.goto()`**: In `action_goto_url`, validate the URL using the existing utilities in `changedetectionio/validate_url.py`. Use `is_private_hostname()` or `is_url_private_or_parser_confused()` to reject URLs resolving to private/reserved IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, x.x.x.x/16). 2. **Restrict URL Scheme**: Limit the URL scheme to `http://` and `https://` only, rejecting `file://`, `javascript:`, `data:`, and other non-HTTP schemes. 3. **Reject URLs with Backslashes**: Prevent parser-differential bypasses by rejecting URLs containing backslashes (already handled by the existing `is_url_private_or_parser_confused()`). 4. **Validate at the Earliest Entry Point**: Apply URL validation to browser step `optional_value` at the `/browsersteps_update` endpoint and when saving browser steps via the watch edit form/API,
User
 herantong (UID 97028)
Submission07/20/2026 04:31 (3 months ago)
Moderation09/22/2026 14:20 (2 months later)
StatusDuplicate
VulDB entry406204 [dgtlmoon changedetection.io up to 0.60.6 Browser Steps optional_value server-side request forgery]
Points0

Might our Artificial Intelligence support you?

Check our Alexa App!