95 lines
3.0 KiB
Python
95 lines
3.0 KiB
Python
"""Validation helpers for request data."""
|
|
from urllib.parse import urlparse
|
|
|
|
# Special sentinel values allowed in allowed_cors_origins
|
|
_CORS_SENTINELS = {"+", "*"}
|
|
|
|
|
|
def validate_cors_origins(origins):
|
|
"""Validate a list of CORS origin values.
|
|
|
|
Accepts:
|
|
None - means "use global CORS config" (pass-through)
|
|
["+"] - derive origins from the client's redirect_uris
|
|
["*"] - allow any origin
|
|
["https://host"] - explicit allow-list of well-formed origins
|
|
|
|
Each non-sentinel entry must be a well-formed origin:
|
|
scheme (http or https) + host + optional port, with NO path,
|
|
query string, or fragment.
|
|
|
|
Returns:
|
|
(validated_value, None) on success, or
|
|
(None, error_message) on failure.
|
|
"""
|
|
if origins is None:
|
|
return None, None
|
|
|
|
if not isinstance(origins, list):
|
|
return None, "allowed_cors_origins must be a list or null"
|
|
|
|
validated = []
|
|
for i, entry in enumerate(origins):
|
|
if not isinstance(entry, str):
|
|
return None, f"allowed_cors_origins[{i}]: expected a string, got {type(entry).__name__}"
|
|
|
|
entry = entry.strip()
|
|
if not entry:
|
|
return None, f"allowed_cors_origins[{i}]: empty string is not allowed"
|
|
|
|
# Sentinel values are accepted as-is
|
|
if entry in _CORS_SENTINELS:
|
|
validated.append(entry)
|
|
continue
|
|
|
|
# Parse and validate as origin
|
|
error = _validate_single_origin(entry, i)
|
|
if error:
|
|
return None, error
|
|
|
|
validated.append(entry)
|
|
|
|
return validated, None
|
|
|
|
|
|
def _validate_single_origin(origin, index):
|
|
"""Validate that a string is a well-formed browser origin.
|
|
|
|
A valid origin is: scheme://host[:port] with no path, query, or fragment.
|
|
Only http and https schemes are accepted.
|
|
|
|
Returns an error message string on failure, or None on success.
|
|
"""
|
|
try:
|
|
parsed = urlparse(origin)
|
|
except Exception:
|
|
return f"allowed_cors_origins[{index}]: '{origin}' is not a valid URL"
|
|
|
|
if parsed.scheme not in ("http", "https"):
|
|
return (
|
|
f"allowed_cors_origins[{index}]: '{origin}' has an invalid scheme "
|
|
f"'{parsed.scheme}'; only 'http' and 'https' are allowed"
|
|
)
|
|
|
|
if not parsed.hostname:
|
|
return f"allowed_cors_origins[{index}]: '{origin}' is missing a hostname"
|
|
|
|
# Origins must not have a path (other than empty or "/"), query, or fragment
|
|
if parsed.path and parsed.path != "/":
|
|
return (
|
|
f"allowed_cors_origins[{index}]: '{origin}' must not contain a path "
|
|
f"(got '{parsed.path}'). Specify only scheme://host[:port]"
|
|
)
|
|
|
|
if parsed.query:
|
|
return (
|
|
f"allowed_cors_origins[{index}]: '{origin}' must not contain a query string"
|
|
)
|
|
|
|
if parsed.fragment:
|
|
return (
|
|
f"allowed_cors_origins[{index}]: '{origin}' must not contain a fragment"
|
|
)
|
|
|
|
return None
|