dgtlmoon/changedetection.io · error · ValueError
{reason}
Error message
{reason} What it means
ValueError raised by validate_fetch_url (the sync variant) when is_fetch_url_allowed(url) rejects the URL; the rejection reason string becomes the exception message. This is changedetection.io's SSRF guard: it blocks private/loopback/link-local addresses, disallowed ports and similar before any fetch is attempted, surfacing the reason to the user as a watch error or HTTP 400.
Source
Thrown at changedetectionio/validate_url.py:315
if is_url_private_or_parser_confused(url):
return False, (
f"Fetch blocked: '{url}' resolves to a private/reserved IP address "
f"or contains a parser-differential payload. "
f"Set ALLOW_IANA_RESTRICTED_ADDRESSES=true to allow."
)
return True, ''
def validate_fetch_url(url):
"""is_fetch_url_allowed() as an assertion - raises ValueError with the reason.
Use at fetch entry points that should abort loudly (the message surfaces to the user as a
watch error or an HTTP 400). Blocks on DNS; from async code use validate_fetch_url_async().
"""
ok, reason = is_fetch_url_allowed(url)
if not ok:
raise ValueError(reason)
async def validate_fetch_url_async(url):
"""validate_fetch_url() with the DNS lookup pushed to a thread so the event loop isn't blocked."""
import asyncio
loop = asyncio.get_running_loop()
ok, reason = await loop.run_in_executor(None, is_fetch_url_allowed, url)
if not ok:
raise ValueError(reason)
def is_llm_api_base_safe(api_base):
"""SSRF guard for the LLM `api_base` setting (GHSA-jrxm-qjfh-g54f).
Returns (ok: bool, reason: str). Empty/None api_base is allowed (cloud providers
don't need it). When ALLOW_IANA_RESTRICTED_ADDRESSES=true the check is bypassed
so operators can intentionally point at local Ollama / vLLM / LM Studio.
View on GitHub (pinned to 5d9c7c6da7)
Solutions
- Use a publicly routable URL for the watch
- If internal monitoring is intended, add the host/IP to the allowed internal ranges setting (or env var controlling is_fetch_url_allowed policy) in your deployment
- From async code call validate_fetch_url_async instead to avoid blocking the loop on DNS
- Catch ValueError and surface the reason string to the user rather than retrying — it is deterministic policy, not transient
Defensive patterns
Strategy: validation
Validate before calling
from changedetectionio.validate_url import is_fetch_url_allowed
ok, reason = is_fetch_url_allowed(url) # non-throwing pre-check
if not ok:
return {'error': reason}, 400 Try / catch
from changedetectionio.validate_url import validate_fetch_url
try:
validate_fetch_url(url)
except ValueError as e:
return {'error': str(e)}, 400 # reason string is user-displayable Prevention
- Use is_fetch_url_allowed(url) for non-throwing checks in batch validation
- Reserve private-IP targets for deployments where the SSRF allow-list is explicitly configured
- Remember the sync variant does blocking DNS — never call it from async code
When it happens
Trigger: A watch URL (or redirect/L webhook target) resolving to 127.0.0.1, 10.x, 192.168.x, 172.16-31.x, 169.254.x, ::1, or a hostname whose DNS points there; also blocked ports/schemes per policy. Blocks on DNS, hence the async variant for event-loop code.
Common situations: Testing against localhost or an internal service from the container; DNS rebind where a public name resolves privately; watchers configured with metadata IPs (169.254.169.254); legitimate internal-monitoring setups blocked by default SSRF policy (can be explicitly allowed).
Related errors
- Redirect blocked: '{redirect_url}' resolves to a private/res
- Watch protocol is not permitted or invalid URL format
- ScreenshotUnavailable
- abort(404)
AI-assisted analysis of dgtlmoon/changedetection.io@5d9c7c6da7 (2026-08-27).
Data as JSON: /api/errors/06009531c5390a79.
Report an issue: GitHub.