GHSA-3c45-4pj5-ch7m: changedetection.io is Vulnerable to SSRF via Watch URLs
SummaryChangedetection.io is vulnerable to Server-Side Request Forgery (SSRF) because the URL validation function `is_safe_valid_url()` does not validate the resolved IP address of watch URLs against private, loopback, or link-local address ranges. An authenticated user (or any user when no password is configured, which is the default) can add a watch for internal network URLs such as:`http://169.254.169.254``http://10.0.0.1/``http://127.0.0.1/`The application fetches these URLs server-side, stores the response content, and makes it viewable through the web UI — enabling full data exfiltration from internal services.This is particularly severe because:The fetched content is stored and viewable - this is not a blind SSRFWatches are fetched periodically - creating a persistent SSRF that continuously accesses internal resourcesBy default, no password is set - the web UI is accessible without authenticationSelf-hosted deployments typically run on cloud infrastructure where `169.254.169.254` returns real IAM credentialsDetailsThe URL validation function `is_safe_valid_url()` in `changedetectionio/validate_url.py` (lines 60–122) validates the URL protocol (http/https/ftp) and format using the `validators` library, but does not perform any DNS resolution or IP address validation:The HTTP fetcher in `changedetectionio/content_fetchers/requests.py` (lines 83–89) then makes the request without any additional IP validation:The response content is stored and made available to the user:This validation gap exists in all entry points that accept watch URLs:Web UI: `changedetectionio/store/__init__.py:718`REST API: `changedetectionio/api/watch.py:163, 428`Import API: `changedetectionio/api/import.py:188`All use the same `is_safe_valid_url()` function, so a single fix addresses all paths.PoCPrerequisitesA changedetection.io instance (Docker deployment)Network access to the instance (default port 5000)Step 1: Deploy changedetection.io with an internal serviceCreate `internal-service.py`:Create `Dockerfile.internal`:Create `docker-compose.yml`:Start the stack:Step 2: Add a watch for the internal serviceOpen `http://localhost:5000/` in a browser (no password required by default).In the URL field, enter:Click **Watch** and wait for the first check to complete.Step 3: View the exfiltrated dataClick on the watch entry, then click **Preview**. The page displays the internal service’s response containing the simulated credentials:Step 4: Verify via API (alternative)Expected output — the internal service’s response containing simulated credentials:In a real cloud deployment, replacing `http://internal-service/` with:would return real AWS IAM credentials.Impact**Who is impacted:**  
All self-hosted changedetection.io deployments, particularly those running on cloud infrastructure (AWS, GCP, Azure) where the instance metadata service at `169.254.169.254` is accessible.**What an attacker can do:****Steal cloud credentials:** Access the cloud metadata endpoint to obtain IAM credentials, service account tokens, or managed identity tokens**Scan internal networks:** Discover internal services by adding watches for internal IP ranges and observing responses**Access internal services:** Read data from internal APIs, databases, and admin interfaces that are not exposed to the internet**Persistent access:** Watches are fetched periodically on a configurable schedule, providing continuous access to internal resources**No authentication required by default:** The web UI has no password set by default, allowing any user with network access to exploit this vulnerabilitySuggested RemediationAdd IP address validation to `is_safe_valid_url()` in `changedetectionio/validate_url.py`:Then add to `is_safe_valid_url()` before the final `return True`:An environment variable (e.g., `ALLOW_PRIVATE_IPS=true`) could be provided for users who intentionally need to monitor internal services.
CVSS v3.1: 8.6