Initial commit: Key Ajans portfolio site

This commit is contained in:
kayasamat
2026-08-07 17:46:14 +03:00
commit 91bb52bce4
18 changed files with 1656 additions and 0 deletions
+182
View File
@@ -0,0 +1,182 @@
"""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