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

  1. Use a publicly routable URL for the watch
  2. 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
  3. From async code call validate_fetch_url_async instead to avoid blocking the loop on DNS
  4. 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

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


AI-assisted analysis of dgtlmoon/changedetection.io@5d9c7c6da7 (2026-08-27). Data as JSON: /api/errors/06009531c5390a79. Report an issue: GitHub.