feat(SEC-001): protect workspace bootstrap with turnstile

This commit is contained in:
Bu5hm4nn
2026-03-25 10:02:10 +01:00
parent f6667b6b63
commit 40f7e74a1b
15 changed files with 323 additions and 34 deletions

73
app/services/turnstile.py Normal file
View File

@@ -0,0 +1,73 @@
from __future__ import annotations
import logging
import os
from dataclasses import dataclass
import requests
TURNSTILE_VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
DEFAULT_TURNSTILE_TEST_SITE_KEY = "1x00000000000000000000AA"
DEFAULT_TURNSTILE_TEST_SECRET_KEY = "1x0000000000000000000000000000000AA"
ALWAYS_FAIL_TURNSTILE_TEST_SITE_KEY = "2x00000000000000000000AB"
ALWAYS_FAIL_TURNSTILE_TEST_SECRET_KEY = "2x0000000000000000000000000000000AA"
logger = logging.getLogger(__name__)
@dataclass(frozen=True)
class TurnstileSettings:
site_key: str
secret_key: str
enabled: bool
uses_test_keys: bool
def _environment() -> str:
return os.getenv("APP_ENV", os.getenv("ENVIRONMENT", "development")).lower()
def load_turnstile_settings() -> TurnstileSettings:
site_key = os.getenv("TURNSTILE_SITE_KEY", "")
secret_key = os.getenv("TURNSTILE_SECRET_KEY", "")
enabled = os.getenv("TURNSTILE_ENABLED", "true").lower() not in {"0", "false", "no"}
env = _environment()
if not site_key or not secret_key:
if env in {"development", "test"}:
site_key = site_key or DEFAULT_TURNSTILE_TEST_SITE_KEY
secret_key = secret_key or DEFAULT_TURNSTILE_TEST_SECRET_KEY
else:
raise RuntimeError("Turnstile keys must be configured outside development/test environments")
uses_test_keys = site_key == DEFAULT_TURNSTILE_TEST_SITE_KEY and secret_key == DEFAULT_TURNSTILE_TEST_SECRET_KEY
return TurnstileSettings(
site_key=site_key,
secret_key=secret_key,
enabled=enabled,
uses_test_keys=uses_test_keys,
)
def verify_turnstile_token(token: str, remote_ip: str | None = None) -> bool:
settings = load_turnstile_settings()
if not settings.enabled:
return True
if not token.strip():
return False
try:
response = requests.post(
TURNSTILE_VERIFY_URL,
data={
"secret": settings.secret_key,
"response": token,
"remoteip": remote_ip or "",
},
timeout=10,
)
response.raise_for_status()
payload = response.json()
except (requests.RequestException, ValueError) as exc:
logger.warning("Turnstile verification failed: %s", exc)
return False
return bool(payload.get("success"))