fix: add OAUTH_REDIRECT_ALLOWLIST for client registration and waive the open default (SEC-05)

This commit is contained in:
Latte committed 2026-10-14 17:24:41 +02:00
1 parent 79aa4f7fc0
commit 8be859243d
7 files changed
+283 -11

No files matched your search

+19 -7
View File
@@ -59,7 +59,7 @@ from pydantic import AnyHttpUrl, ValidationError
from aegis_gitea_mcp.audit import AuditLogger, get_audit_logger
from aegis_gitea_mcp.auth import MCP_SCOPE
from aegis_gitea_mcp.config import Settings
from aegis_gitea_mcp.config import Settings, redirect_matches_allowlist
from aegis_gitea_mcp.oauth.store import (
ClientRecord,
OAuthStore,
@@ -87,6 +87,7 @@ CONSENT_PATH = "/oauth/consent"
_LOOPBACK_REDIRECT_HOSTS = frozenset({"localhost", "127.0.0.1", "::1"})
_CLIENT_AUTH_METHODS = frozenset({"none", "client_secret_post", "client_secret_basic"})
_NOT_ALLOWED = "redirect URI is not on this server's allowlist (OAUTH_REDIRECT_ALLOWLIST)"
_REDIRECT_RULE = (
"redirect URIs must use https://, or http:// on localhost, 127.0.0.1 or [::1], "
"without credentials or a fragment"
@@ -96,8 +97,12 @@ V = TypeVar("V")
Clock = Callable[[], float]
def redirect_uri_problem(uri: str) -> str | None:
"""Return why ``uri`` is not an acceptable redirect URI, or None when it is."""
def redirect_uri_problem(uri: str, allowlist: list[str] | None = None) -> str | None:
"""Return why ``uri`` is not an acceptable redirect URI, or None when it is.
With a non-empty ``allowlist`` an https URI must also match it; http loopback
URIs (native clients on the user's own machine) stay allowed.
"""
try:
parts = urlsplit(uri)
parts.port # noqa: B018 (accessing .port validates it)
@@ -108,10 +113,10 @@ def redirect_uri_problem(uri: str) -> str | None:
host = (parts.hostname or "").lower()
if not host:
return _REDIRECT_RULE
if parts.scheme == "https":
return None
if parts.scheme == "http" and host in _LOOPBACK_REDIRECT_HOSTS:
return None
if parts.scheme == "https":
return None if redirect_matches_allowlist(uri, allowlist or []) else _NOT_ALLOWED
return _REDIRECT_RULE
@@ -306,7 +311,8 @@ class GiteaOAuthProvider(
async def register_client(self, client_info: OAuthClientInformationFull) -> None:
"""Validate and persist a dynamically registered client.
Redirect URIs must be https, or http on a loopback host. When
Redirect URIs must be https, or http on a loopback host, and match
``OAUTH_REDIRECT_ALLOWLIST`` when that is set. When
:data:`MAX_CLIENTS` is reached, the least recently used client without
any session is evicted; when every client has a session, registration
is refused.
@@ -320,7 +326,7 @@ class GiteaOAuthProvider(
"invalid_redirect_uri", f"between 1 and {MAX_REDIRECT_URIS} redirect URIs required"
)
for uri in uris:
problem = redirect_uri_problem(uri)
problem = redirect_uri_problem(uri, self._settings.oauth_redirect_allowlist)
if problem is not None:
raise RegistrationError("invalid_redirect_uri", problem)
if client_info.token_endpoint_auth_method not in _CLIENT_AUTH_METHODS:
@@ -368,6 +374,12 @@ class GiteaOAuthProvider(
"""
if params.resource is not None and not same_resource(params.resource, self.resource):
raise AuthorizeError("invalid_target", "unknown resource")
# A client registered before the allowlist was set must not slip through it.
if (
redirect_uri_problem(str(params.redirect_uri), self._settings.oauth_redirect_allowlist)
is not None
):
raise AuthorizeError("invalid_request", "redirect URI is not allowed")
request_id = secrets.token_urlsafe(32)
pending = PendingAuthorization(
client_id=client.client_id,