feat(SEC-001): protect workspace bootstrap with turnstile
This commit is contained in:
73
app/services/turnstile.py
Normal file
73
app/services/turnstile.py
Normal 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"))
|
||||
Reference in New Issue
Block a user