Validate origin hostname shape: no empty labels (leading/trailing/double dots rejected) in stored config, sanitize, and Passkey construction

This commit is contained in:
2026-09-07 16:50:40 +00:00
parent 04d6768ba6
commit cb609abe5f
4 changed files with 67 additions and 6 deletions
+9 -4
View File
@@ -218,7 +218,9 @@ def validate_config(
)
if hostutil.is_wildcard_pattern(key):
base = hostutil.wildcard_base(key)
if not base or not hostutil.is_subdomain(base, rp_id):
if not base or not hostutil.is_valid_hostname(base):
raise ValueError(f"Invalid wildcard origin: '{key}'")
if not hostutil.is_subdomain(base, rp_id):
raise ValueError(
f"Origin '{key}' is a wildcard outside the rp-id "
f"domain '{rp_id}' — related origins must be "
@@ -228,7 +230,7 @@ def validate_config(
raise ValueError(f"Wildcard origin '{key}' cannot be the auth host")
continue
hn = hostutil.origin_hostname(origin_url(key))
if not hn:
if not hn or not hostutil.is_valid_hostname(hn):
raise ValueError(f"Invalid origin: '{key}'")
if hostutil.is_subdomain(hn, rp_id):
if is_auth:
@@ -322,7 +324,10 @@ def sanitize_config(
continue
if hostutil.is_wildcard_pattern(key):
base = hostutil.wildcard_base(key)
if not base or not hostutil.is_subdomain(base, rp_id):
if not base or not hostutil.is_valid_hostname(base):
warn(f"Domain '{rp_id}': invalid wildcard origin '{key}' dropped")
continue
if not hostutil.is_subdomain(base, rp_id):
warn(
f"Domain '{rp_id}': origin '{key}' is a wildcard "
"outside the rp-id domain — dropped (related origins "
@@ -338,7 +343,7 @@ def sanitize_config(
origins[key] = props
continue
hn = hostutil.origin_hostname(origin_url(key))
if not hn:
if not hn or not hostutil.is_valid_hostname(hn):
warn(f"Domain '{rp_id}': invalid origin '{key}' dropped")
continue
if is_auth:
+7 -2
View File
@@ -81,6 +81,8 @@ class Passkey:
for o in origins or []:
if hostutil.is_wildcard_pattern(o):
hostname = hostutil.origin_hostname(o)
if hostname and not hostutil.is_valid_hostname(hostname):
raise ValueError(f"Origin '{o}' has a malformed hostname")
else:
self._validate_origin_url(o)
hostname = hostutil.origin_hostname(o)
@@ -113,9 +115,12 @@ class Passkey:
@staticmethod
def _validate_origin_url(origin: str) -> None:
"""Validate that an origin URL is well-formed (has a hostname)."""
if not hostutil.origin_hostname(origin):
"""Validate that an origin URL is well-formed (has a valid hostname)."""
hostname = hostutil.origin_hostname(origin)
if not hostname:
raise ValueError(f"Invalid origin URL: no hostname found in '{origin}'")
if not hostutil.is_valid_hostname(hostname):
raise ValueError(f"Invalid origin URL: malformed hostname in '{origin}'")
def _origin_in_subtree(self, origin: str) -> bool:
"""Check whether an origin's hostname is the rp-id or its subdomain."""
+7
View File
@@ -18,6 +18,13 @@ def validate_rp_id(rp_id: str) -> None:
raise ValueError(f"rp_id '{rp_id}' is not a valid domain name")
def is_valid_hostname(hostname: str) -> bool:
"""Check hostname shape: dot-separated alphanumeric/hyphen labels —
no empty labels, so no leading/trailing or double dots ('.localhost',
'localhost.', 'a..b.com' are all malformed)."""
return bool(_RP_ID_RE.match(hostname))
def is_wildcard_pattern(value: str) -> bool:
"""Check whether an origins entry is a wildcard pattern like
'*.example.com' (one subdomain level) or '**.example.com' (the base
+44
View File
@@ -222,6 +222,20 @@ class TestValidateConfig:
Config(domains={"a.com": DomainConfig(origins={"*": True})})
)
def test_malformed_origin_hostname_rejected(self):
"""No empty hostname labels — leading, trailing and double dots
are invalid, in concrete entries and wildcard bases alike."""
for key in (".a.com", "a..com", "a.com.", "http://.a.com:8080"):
with pytest.raises(ValueError, match="Invalid origin"):
domains.validate_config(
Config(domains={"a.com": DomainConfig(origins={key: True})})
)
for key in ("*..a.com", "**..a.com"):
with pytest.raises(ValueError, match="Invalid wildcard origin"):
domains.validate_config(
Config(domains={"a.com": DomainConfig(origins={key: True})})
)
def test_subdomain_entry_is_in_domain(self):
"""An entry within the rp-id domain is an ordinary in-domain
sign-in site, never a related origin."""
@@ -395,6 +409,27 @@ class TestSanitizeConfig:
assert config.domains["a.com"].origins == {}
assert warnings
def test_malformed_hostname_dropped(self):
"""Empty hostname labels (leading/trailing/double dots) are dropped,
from concrete entries and wildcard bases alike."""
config, warnings = domains.sanitize_config(
Config(
domains={
"a.com": DomainConfig(
origins={
".a.com": True,
"a.com.": True,
"**.a..com": True,
"ok.a.com": True,
}
)
}
)
)
assert list(config.domains["a.com"].origins) == ["ok.a.com"]
assert len(warnings) == 3
domains.validate_config(config) # sanitized config is strict-clean
def test_invalid_rp_id_domain_dropped(self):
config, warnings = domains.sanitize_config(
Config(domains={"not a domain!": DomainConfig(), "ok.com": DomainConfig()})
@@ -558,6 +593,15 @@ class TestOriginValidation:
with pytest.raises(ValueError, match="Invalid origin"):
Passkey(rp_id="example.com", origins=["*"])
def test_malformed_hostname_rejected(self):
"""Leading/trailing/double dots are invalid in any entry form."""
with pytest.raises(ValueError, match="malformed hostname"):
Passkey(rp_id="example.com", origins=["https://.example.com"])
with pytest.raises(ValueError, match="malformed hostname"):
Passkey(rp_id="example.com", origins=["**.a..example.com"])
with pytest.raises(ValueError, match="malformed hostname"):
Passkey(rp_id="example.com", related_origins=["https://other..com"])
def test_localhost_wildcard_matches_any_scheme_and_port(self):
"""Under localhost, wildcards match any scheme and any port."""
p = Passkey(rp_id="localhost", origins=["**.localhost"])