{"record":{"id":"21e25549413cc0ba","repo":"dgtlmoon/changedetection.io","slug":"watch-protocol-is-not-permitted-or-invalid-url-for","errorCode":null,"errorMessage":"Watch protocol is not permitted or invalid URL format","messagePattern":"Watch protocol is not permitted or invalid URL format","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"changedetectionio/forms.py","lineNumber":625,"sourceCode":"class validateURL(object):\n\n    \"\"\"\n       Flask wtform validators wont work with basic auth\n    \"\"\"\n\n    def __init__(self, message=None):\n        self.message = message\n\n    def __call__(self, form, field):\n        # This should raise a ValidationError() or not\n        validate_url(field.data)\n\n\ndef validate_url(test_url):\n    from changedetectionio.validate_url import is_safe_valid_url\n    if not is_safe_valid_url(test_url):\n        # This should be wtforms.validators.\n        raise ValidationError('Watch protocol is not permitted or invalid URL format')\n\n\nclass validateLLMApiBaseSafe(object):\n    \"\"\"Block private/loopback/reserved api_base values (SSRF) unless the operator\n    has opted in via ALLOW_IANA_RESTRICTED_ADDRESSES=true.\"\"\"\n\n    def __call__(self, form, field):\n        from changedetectionio.validate_url import is_llm_api_base_safe\n        ok, reason = is_llm_api_base_safe(field.data)\n        if not ok:\n            raise ValidationError(reason)\n\n\nclass ValidateSinglePythonRegexString(object):\n    def __init__(self, message=None):\n        self.message = message\n\n    def __call__(self, form, field):","sourceCodeStart":607,"sourceCodeEnd":643,"githubUrl":"https://github.com/dgtlmoon/changedetection.io/blob/5d9c7c6da76340597243e8163c4f2439237fa0e8/changedetectionio/forms.py#L607-L643","documentation":"validate_url() in changedetectionio/forms.py calls is_safe_valid_url() from changedetectionio.validate_url and raises wtforms.ValidationError when the URL fails that check. The check enforces both URL syntax (via urlparse/validators) and a protocol allow-list, blocking schemes like file://, javascript://, and other non-http(s) protocols.","triggerScenarios":"Calling validate_url(test_url) (directly or via a wtforms field validator) with a URL whose scheme is not http/https, is malformed, or fails is_safe_valid_url's parsing — e.g. 'ftp://example.com', 'file:///etc/passwd', 'javascript:alert(1)', or a string with no scheme at all.","commonSituations":"Users entering non-http schemes in the watch URL field, missing scheme ('example.com/page'), whitespace/control characters, or integrations passing un-normalized URLs. Also used as an SSRF guard so private/unsafe schemes are rejected.","solutions":["Prefix the URL with http:// or https:// and ensure it parses as an absolute URL","Remove disallowed schemes (file:, ftp:, javascript:, data:) — only http/https are permitted","Trim whitespace/quotes around the URL before submitting","If integrating programmatically, pre-check with changedetectionio.validate_url.is_safe_valid_url before calling the API"],"exampleFix":"# before\nvalidate_url('example.com/page')\n# after\nvalidate_url('https://example.com/page')\n","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\ndef url_ok(u: str) -> bool:\n    p = urlparse(u.strip())\n    return p.scheme in ('http', 'https') and bool(p.netloc)\n\n# or use the library's own check:\nfrom changedetectionio.validate_url import is_safe_valid_url\nassert is_safe_valid_url('https://example.com')\n","typeGuard":"def is_http_url(u: str) -> bool:\n    p = urlparse(u.strip())\n    return p.scheme in ('http', 'https') and bool(p.netloc)\n","tryCatchPattern":"from wtforms import ValidationError\ntry:\n    validate_url(url)\nexcept ValidationError as e:\n    flash(str(e))\n","preventionTips":["Always submit absolute http(s) URLs","Normalize with urljoin/urllib.parse before sending to the API","Strip whitespace and quotes from pasted URLs"],"tags":["url-validation","ssrf","wtforms","changedetectionio","protocol-allowlist"],"backgroundTag":"url-scheme-not-allowed","analyzedSha":"5d9c7c6da76340597243e8163c4f2439237fa0e8","analyzedAt":"2026-08-27T19:41:16.067Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}