aboutsummaryrefslogtreecommitdiff
path: root/webmentions_ssg/forms
diff options
context:
space:
mode:
authorDennis Fink2026-08-10 19:36:49 +0200
committerDennis Fink2026-08-10 19:36:49 +0200
commit8350bef3e3baea6042ed3780a054cc65707e9f8d (patch)
tree81ab5f102045f53d23f89e22a41b2f6a2eef9b52 /webmentions_ssg/forms
parentafcdb411882a6c157c3b5008e1c633d3e40cd984 (diff)
downloadwebmentions-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/forms/__init__.py3
-rw-r--r--webmentions_ssg/forms/validators.py25
2 files changed, 27 insertions, 1 deletions
diff --git a/webmentions_ssg/forms/__init__.py b/webmentions_ssg/forms/__init__.py
index 98962ad..0c54999 100644
--- a/webmentions_ssg/forms/__init__.py
+++ b/webmentions_ssg/forms/__init__.py
@@ -4,7 +4,7 @@ from flask_wtf import FlaskForm
from wtforms import PasswordField, StringField, SubmitField
from wtforms.validators import URL, InputRequired, Regexp
-from .validators import AllowedHostname, NotEqualTo
+from .validators import AllowedHostname, NotEqualTo, PublicURL
class LoginForm(FlaskForm):
@@ -25,6 +25,7 @@ class EndpointForm(FlaskForm):
message="source must begin with http or https",
),
NotEqualTo("target"),
+ PublicURL(),
],
)
target = StringField(
diff --git a/webmentions_ssg/forms/validators.py b/webmentions_ssg/forms/validators.py
index 20d9ebf..e979644 100644
--- a/webmentions_ssg/forms/validators.py
+++ b/webmentions_ssg/forms/validators.py
@@ -3,6 +3,12 @@ from urllib.parse import urlsplit
from flask import current_app
from wtforms import ValidationError
+from ..url_security import (
+ AddressResolutionError,
+ NonPublicAddressError,
+ ensure_public_url,
+)
+
class NotEqualTo:
"""
@@ -60,3 +66,22 @@ class AllowedHostname:
message = field.gettext("Invalid input.")
raise ValidationError(message)
+
+
+class PublicURL:
+ def __init__(self, message=None):
+ self.message = message
+
+ def __call__(self, form, field):
+ try:
+ ensure_public_url(field.data)
+ except (
+ AddressResolutionError,
+ NonPublicAddressError,
+ ) as exc:
+ message = self.message
+
+ if message is None:
+ message = field.gettext("URL must resolve to a public address.")
+
+ raise ValidationError(message) from exc