dgtlmoon/changedetection.io · critical · Exception

Redirect blocked: '{redirect_url}' resolves to a private/res

Error message

Redirect blocked: '{redirect_url}' resolves to a private/reserved IP address or contains a parser-differential payload.

What it means

The requests fetcher manually follows redirects (allow_redirects=False) and blocks any Location target that resolves to a private/reserved IP or looks like a parser-differential (SSRF) payload, when allow_iana_restricted is off.

Source

Thrown at changedetectionio/content_fetchers/requests.py:116

                                data=request_body.encode('utf-8') if type(request_body) is str else request_body,
                                url=url,
                                headers=request_headers,
                                timeout=timeout,
                                proxies=proxies,
                                verify=False,
                                allow_redirects=False)

            # Manually follow redirects so each hop's resolved IP can be validated,
            # preventing SSRF via an open redirect on a public host.
            current_url = url
            for _ in range(10):
                if not r.is_redirect:
                    break
                location = r.headers.get('Location', '')
                redirect_url = urljoin(current_url, location)
                if not allow_iana_restricted:
                    if is_url_private_or_parser_confused(redirect_url):
                        raise Exception(f"Redirect blocked: '{redirect_url}' resolves to a private/reserved IP address "
                                        f"or contains a parser-differential payload.")
                current_url = redirect_url
                r = session.request('GET', redirect_url,
                                    headers=request_headers,
                                    timeout=timeout,
                                    proxies=proxies,
                                    verify=False,
                                    allow_redirects=False)
            else:
                raise Exception("Too many redirects")

        except Exception as e:
            msg = str(e)
            if proxies and 'SOCKSHTTPSConnectionPool' in msg:
                msg = f"Proxy connection failed? {msg}"
            raise Exception(msg) from e

        # If the response did not tell us what encoding format to expect, Then use chardet to override what `requests` thinks.

View on GitHub (pinned to 5d9c7c6da7)

Solutions

  1. Verify the redirect chain (curl -IL) and where Location points
  2. If the internal target is legitimate in your network, configure the fetcher to allow it / set allow_iana_restricted appropriately
  3. Re-check the watch URL — a changed or hijacked domain may be redirecting somewhere unexpected
Defensive patterns

Strategy: try-catch

Validate before calling

# Resolve redirect target host pre-check (mirror of guard)
import socket, ipaddress, urllib.parse
loc = urljoin(url, response.headers.get('Location',''))
host = urllib.parse.urlparse(loc).hostname
ip = socket.gethostbyname(host)
assert not ipaddress.ip_address(ip).is_private, 'redirect would be blocked'

Try / catch

try:
    fetcher.run()
except Exception as e:
    if 'Redirect blocked' in str(e):
        log_security_event(str(e))  # potential SSRF attempt by target site
        disable_watch_or_alert()

Prevention

When it happens

Trigger: A watched public URL redirecting to http://127.0.0.1, 10.x, 169.254.169.254, or a URL with mixed encodings that parsers resolve differently.

Common situations: Sites redirecting to internal hosts; malicious or compromised target attempting SSRF via redirect; legitimate services redirecting to internal hostnames in on-prem networks.

Related errors


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