From 8350bef3e3baea6042ed3780a054cc65707e9f8d Mon Sep 17 00:00:00 2001 From: Dennis Fink Date: Mon, 10 Aug 2026 19:36:49 +0200 Subject: fix(receiver): prevent requests to non-public addresses Resolve source hostnames before fetching and reject addresses that are not globally routable to prevent SSRF against local or private services. Repeat the check for every HTTP request so redirects cannot bypass the initial source validation. Treat DNS resolution failures during verification as temporary fetch errors. --- webmentions_ssg/url_security.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 webmentions_ssg/url_security.py (limited to 'webmentions_ssg/url_security.py') diff --git a/webmentions_ssg/url_security.py b/webmentions_ssg/url_security.py new file mode 100644 index 0000000..ce5d700 --- /dev/null +++ b/webmentions_ssg/url_security.py @@ -0,0 +1,37 @@ +import ipaddress +from urllib.parse import urlsplit + +import dns.exception +import dns.resolver + + +class AddressResolutionError(Exception): + pass + + +class NonPublicAddressError(Exception): + pass + + +def ensure_public_url(url: str) -> None: + hostname = urlsplit(url).hostname + + if hostname is None: + raise NonPublicAddressError("URL has no hostname") + + try: + answers = dns.resolver.resolve_name(hostname) + except dns.exception.DNSException as exc: + raise AddressResolutionError( + f"Could not resolve hostname {hostname!r}" + ) from exc + + addresses = {ipaddress.ip_address(address) for address in answers.addresses()} + + if not addresses: + raise AddressResolutionError(f"Hostname {hostname!r} did not resolve") + + if any(not address.is_global for address in addresses): + raise NonPublicAddressError( + f"Hostname {hostname!r} resolves to a non-public address" + ) -- cgit v1.3.1