"""Güvenlik katmanı: admin config, CSRF, rate limit, sanitizasyon, başlıklar.""" import hmac import json import os import re import secrets import threading import time from flask import abort, request, session # --------------------------------------------------------------------------- # Admin kimlik bilgileri PROJE DIŞINDA tutulur: # ~/.config/key-ajansi/admins.json # (KEY_AJANSI_ADMINS ortam değişkeniyle başka bir yola taşınabilir) # --------------------------------------------------------------------------- DEFAULT_CONFIG_PATH = os.path.join( os.path.expanduser("~"), ".config", "key-ajansi", "admins.json" ) CONFIG_PATH = os.environ.get("KEY_AJANSI_ADMINS", DEFAULT_CONFIG_PATH) def load_server_config(): if not os.path.exists(CONFIG_PATH): raise RuntimeError( "Admin yapılandırması bulunamadı. Önce şunu çalıştırın:\n" " python3 setup_admins.py\n" f"Beklenen dosya: {CONFIG_PATH}" ) with open(CONFIG_PATH, "r", encoding="utf-8") as fh: data = json.load(fh) if not data.get("secret") or not isinstance(data.get("admins"), list): raise RuntimeError(f"{CONFIG_PATH} dosyası hatalı; setup_admins.py ile yeniden oluşturun.") return data # --------------------------------------------------------------------------- # Rate limiting (DDoS / brute-force koruması) # --------------------------------------------------------------------------- _limits = {} _lock = threading.Lock() def _prune_limits(now): if len(_limits) > 10000: for key in list(_limits): if now - _limits[key][0] > 3600: del _limits[key] def check_limit(scope, limit, window): """scope başına IP'ye limit uygula; aşıldıysa False döner.""" key = f"{scope}:{request.remote_addr}" now = time.time() with _lock: _prune_limits(now) bucket = _limits.get(key) if bucket is None or now - bucket[0] > window: _limits[key] = [now, 1] return True if bucket[1] >= limit: return False bucket[1] += 1 return True def rate_limited(scope, limit, window): def deco(fn): def wrapper(*args, **kwargs): if not check_limit(scope, limit, window): abort(429) return fn(*args, **kwargs) wrapper.__name__ = fn.__name__ return wrapper return deco # --------------------------------------------------------------------------- # CSRF koruması # --------------------------------------------------------------------------- def ensure_csrf_token(): token = session.get("csrf_token") if not token: token = secrets.token_urlsafe(32) session["csrf_token"] = token return token def validate_csrf(): token = request.form.get("csrf_token") or request.headers.get("X-CSRF-Token") expected = session.get("csrf_token") if not token or not expected: return False return hmac.compare_digest(str(token), str(expected)) # --------------------------------------------------------------------------- # Girdi sanitizasyonu / doğrulaması (XSS, çok uzun girdiler) # --------------------------------------------------------------------------- _USERNAME_RE = re.compile(r"^[A-Za-z0-9_.-]{3,32}$") _EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$") _CONTROL_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]") def clean_text(value, max_len): """Kontrol karakterlerini temizle, kırp, uzunluğu sınırla.""" if value is None: return "" value = str(value) value = _CONTROL_RE.sub("", value) value = value.strip() return value[:max_len] def clean_multiline(value, max_len): return clean_text(value, max_len) def validate_username(value): value = clean_text(value, 32) if not _USERNAME_RE.match(value): return None return value def validate_email(value): value = clean_text(value, 120) if len(value) > 120 or not _EMAIL_RE.match(value): return None return value.lower() def validate_link(value): value = clean_text(value, 500) if not value: return "" if not re.match(r"^https?://[^\s]+$", value): return None if len(value) > 500: return None return value # --------------------------------------------------------------------------- # İstek öncesi / sonrası filtreler # --------------------------------------------------------------------------- def before_request(): if request.path.startswith("/static"): return None if not check_limit("global", 600, 300): abort(429) if request.method in ("POST", "PUT", "PATCH", "DELETE"): if not validate_csrf(): abort(400, description="CSRF doğrulaması başarısız.") return None CSP = ( "default-src 'self'; " "script-src 'self'; " "style-src 'self' https://fonts.googleapis.com; " "font-src 'self' https://fonts.gstatic.com; " "img-src 'self' data: https://picsum.photos; " "connect-src 'self'; " "frame-ancestors 'none'; " "base-uri 'self'; form-action 'self'; object-src 'none'" ) def after_request(response): response.headers.setdefault("X-Content-Type-Options", "nosniff") response.headers.setdefault("X-Frame-Options", "DENY") response.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin") response.headers.setdefault("X-XSS-Protection", "0") response.headers.setdefault("Permissions-Policy", "camera=(), geolocation=(), microphone=()") response.headers.setdefault("Content-Security-Policy", CSP) if request.path.startswith("/admin"): response.headers["Cache-Control"] = "no-store, max-age=0" return response