diff options
| author | Dennis Fink | 2026-08-10 19:36:49 +0200 |
|---|---|---|
| committer | Dennis Fink | 2026-08-10 19:36:49 +0200 |
| commit | 8350bef3e3baea6042ed3780a054cc65707e9f8d (patch) | |
| tree | 81ab5f102045f53d23f89e22a41b2f6a2eef9b52 /webmentions_ssg/tasks/receiver.py | |
| parent | afcdb411882a6c157c3b5008e1c633d3e40cd984 (diff) | |
| download | webmentions-ssg-8350bef3e3baea6042ed3780a054cc65707e9f8d.tar.gz webmentions-ssg-8350bef3e3baea6042ed3780a054cc65707e9f8d.zip | |
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.
Diffstat (limited to '')
| -rw-r--r-- | webmentions_ssg/tasks/receiver.py | 18 |
1 files changed, 18 insertions, 0 deletions
diff --git a/webmentions_ssg/tasks/receiver.py b/webmentions_ssg/tasks/receiver.py index ea48299..9475866 100644 --- a/webmentions_ssg/tasks/receiver.py +++ b/webmentions_ssg/tasks/receiver.py @@ -10,6 +10,11 @@ from .. import APP_NAME, VERSION from .. import DATABASE as db from .. import HUEY as huey from ..models import ReceivedWebmention +from ..url_security import ( + AddressResolutionError, + NonPublicAddressError, + ensure_public_url, +) class VerificationError(Exception): @@ -103,6 +108,16 @@ def text_mentions_target(body: str, target_url: str) -> bool: return any(match.group() == target_url for match in IRI_PATTERN.finditer(body)) +def ensure_public_request(request: httpx.Request) -> None: + """Prevent requests to non-public network addresses.""" + try: + ensure_public_url(str(request.url)) + except NonPublicAddressError as exc: + raise VerificationError("Source resolves to a non-public address") from exc + except AddressResolutionError as exc: + raise TemporaryFetchError("Source hostname could not be resolved") from exc + + def fetch_source(source_url: str) -> tuple[httpx.Response, bytes]: """Fetch a source with limits on redirects, time, and response size.""" @@ -117,6 +132,9 @@ def fetch_source(source_url: str) -> tuple[httpx.Response, bytes]: follow_redirects=True, max_redirects=current_app.config.get("WEBMENTIONS_SSG_MAX_REDIRECTS", 20), trust_env=False, + event_hooks={ + "request": [ensure_public_request], + }, ) as client: with client.stream("GET", source_url) as response: match response.status_code: |
