fix: reject whitespace and control characters in base URLs and warn on plain http Gitea (SEC-10)
This commit is contained in:
1 parent
87f048cc4d
commit
d96c3ac604
5 files changed
+284
-5
No files matched your search
@@ -109,6 +109,13 @@ def run_serve(args: argparse.Namespace) -> int:
|
||||
if settings is None:
|
||||
return 2
|
||||
configure_logging(settings.log_level)
|
||||
if settings.insecure_gitea_url():
|
||||
print(
|
||||
"aegis-gitea-mcp: warning: GITEA_URL uses plain http to a host that is not loopback: "
|
||||
"the Gitea token, the OAuth client secret and every user's token travel unencrypted. "
|
||||
"Use https",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if settings.mcp_transport == "http":
|
||||
return transports.run_http(settings)
|
||||
return transports.run_stdio(settings)
|
||||
|
||||
@@ -15,7 +15,7 @@ import sys
|
||||
from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any, Literal
|
||||
from urllib.parse import urlsplit
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
from pydantic import Field, SecretStr, field_validator
|
||||
from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict
|
||||
@@ -147,8 +147,16 @@ def redirect_matches_allowlist(uri: str, allowlist: list[str]) -> bool:
|
||||
|
||||
|
||||
def _normalize_http_url(value: str, *, field: str) -> str:
|
||||
"""Validate an http(s) URL and return it without query, fragment or trailing slash."""
|
||||
"""Validate an http(s) URL and return it rebuilt without query, fragment or trailing slash.
|
||||
|
||||
The result is assembled from the parsed parts, so the string that is validated
|
||||
is the string that is used. ``urlsplit`` silently drops tabs and newlines
|
||||
before parsing, so any whitespace, backslash or non-printable character
|
||||
(control and bidi characters included) is rejected up front.
|
||||
"""
|
||||
candidate = value.strip()
|
||||
if any(ch.isspace() or not ch.isprintable() or ch == "\\" for ch in candidate):
|
||||
raise ValueError(f"{field} must not contain whitespace, backslashes or control characters")
|
||||
parts = urlsplit(candidate)
|
||||
if parts.scheme not in {"http", "https"}:
|
||||
raise ValueError(f"{field} must start with http:// or https://")
|
||||
@@ -162,7 +170,7 @@ def _normalize_http_url(value: str, *, field: str) -> str:
|
||||
parts.port # noqa: B018 (accessing .port validates it)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"{field} has an invalid port") from exc
|
||||
return candidate.rstrip("/")
|
||||
return urlunsplit((parts.scheme, parts.netloc, parts.path.rstrip("/"), "", ""))
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
@@ -403,6 +411,11 @@ class Settings(BaseSettings):
|
||||
"""Audit log path, defaulting to ``DATA_DIR/audit.log``."""
|
||||
return self.audit_log_path or (self.data_dir / "audit.log")
|
||||
|
||||
def insecure_gitea_url(self) -> bool:
|
||||
"""True when ``GITEA_URL`` is plain http to a host that is not loopback."""
|
||||
parts = urlsplit(self.gitea_url)
|
||||
return parts.scheme == "http" and not is_loopback_host(parts.hostname or "")
|
||||
|
||||
def is_loopback_bind(self) -> bool:
|
||||
"""Return True when ``MCP_HOST`` is a loopback address (127.0.0.0/8, ::1, localhost)."""
|
||||
return is_loopback_host(self.mcp_host)
|
||||
|
||||
Reference in new issue
Block a user