{"record":{"id":"06009531c5390a79","repo":"dgtlmoon/changedetection.io","slug":"reason","errorCode":null,"errorMessage":"{reason}","messagePattern":"\\{reason\\}","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"changedetectionio/validate_url.py","lineNumber":315,"sourceCode":"        if is_url_private_or_parser_confused(url):\n            return False, (\n                f\"Fetch blocked: '{url}' resolves to a private/reserved IP address \"\n                f\"or contains a parser-differential payload. \"\n                f\"Set ALLOW_IANA_RESTRICTED_ADDRESSES=true to allow.\"\n            )\n\n    return True, ''\n\n\ndef validate_fetch_url(url):\n    \"\"\"is_fetch_url_allowed() as an assertion - raises ValueError with the reason.\n\n    Use at fetch entry points that should abort loudly (the message surfaces to the user as a\n    watch error or an HTTP 400). Blocks on DNS; from async code use validate_fetch_url_async().\n    \"\"\"\n    ok, reason = is_fetch_url_allowed(url)\n    if not ok:\n        raise ValueError(reason)\n\n\nasync def validate_fetch_url_async(url):\n    \"\"\"validate_fetch_url() with the DNS lookup pushed to a thread so the event loop isn't blocked.\"\"\"\n    import asyncio\n    loop = asyncio.get_running_loop()\n    ok, reason = await loop.run_in_executor(None, is_fetch_url_allowed, url)\n    if not ok:\n        raise ValueError(reason)\n\n\ndef is_llm_api_base_safe(api_base):\n    \"\"\"SSRF guard for the LLM `api_base` setting (GHSA-jrxm-qjfh-g54f).\n\n    Returns (ok: bool, reason: str). Empty/None api_base is allowed (cloud providers\n    don't need it). When ALLOW_IANA_RESTRICTED_ADDRESSES=true the check is bypassed\n    so operators can intentionally point at local Ollama / vLLM / LM Studio.\n","sourceCodeStart":297,"sourceCodeEnd":333,"githubUrl":"https://github.com/dgtlmoon/changedetection.io/blob/5d9c7c6da76340597243e8163c4f2439237fa0e8/changedetectionio/validate_url.py#L297-L333","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"from changedetectionio.validate_url import is_fetch_url_allowed\nok, reason = is_fetch_url_allowed(url)  # non-throwing pre-check\nif not ok:\n    return {'error': reason}, 400","typeGuard":null,"tryCatchPattern":"from changedetectionio.validate_url import validate_fetch_url\ntry:\n    validate_fetch_url(url)\nexcept ValueError as e:\n    return {'error': str(e)}, 400  # reason string is user-displayable","preventionTips":["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"],"tags":["ssrf","url-validation","security","fetch"],"backgroundTag":"ssrf-url-blocked","analyzedSha":"5d9c7c6da76340597243e8163c4f2439237fa0e8","analyzedAt":"2026-08-27T19:41:16.067Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}