"feat: Integrate rate limiter

This commit is contained in:
Till Tomczak 2025-05-29 15:38:32 +02:00
parent d6a583d115
commit 9c89ad1397
5 changed files with 644 additions and 51 deletions

Binary file not shown.

Binary file not shown.

View File

@ -3,30 +3,48 @@
{% block title %}Drucker - Mercedes-Benz MYP Platform{% endblock %} {% block title %}Drucker - Mercedes-Benz MYP Platform{% endblock %}
{% block content %} {% block content %}
<div class="bg-professional"> <div class="bg-professional" style="background: #f8fafc !important;">
<!-- Dark Mode Override -->
<style>
.dark .bg-professional {
background: #000000 !important;
}
.dark .professional-hero {
background: linear-gradient(135deg, #000000 0%, #1a1a1a 100%) !important;
border-color: #333333 !important;
}
.dark .professional-container {
background: #111111 !important;
border-color: #333333 !important;
}
.dark .mb-glass {
background: rgba(17, 17, 17, 0.95) !important;
border-color: rgba(255, 255, 255, 0.1) !important;
}
</style>
<!-- Professional Hero Header --> <!-- Professional Hero Header -->
<div class="professional-hero hero-pattern animate-fade-in"> <div class="professional-hero hero-pattern animate-fade-in" style="margin: 2rem; margin-bottom: 3rem;">
<div class="absolute inset-0 bg-gradient-to-r from-black/10 to-black/20 dark:from-black/20 dark:to-black/40"></div> <div class="absolute inset-0 bg-gradient-to-r from-black/10 to-black/20 dark:from-black/40 dark:to-black/60"></div>
<!-- Status Indicator --> <!-- Status Indicator -->
<div class="absolute top-6 right-6 flex items-center space-x-3 z-10"> <div class="absolute top-6 right-6 flex items-center space-x-4 z-10">
<div class="mb-glass rounded-full px-4 py-2 animate-scale-in"> <div class="mb-glass rounded-full px-6 py-3 animate-scale-in">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-3">
<div class="status-dot status-online"></div> <div class="status-dot status-online"></div>
<span class="text-sm font-semibold text-professional-primary">Live</span> <span class="text-sm font-semibold text-professional-primary">Live Status</span>
</div> </div>
</div> </div>
<div class="mb-glass rounded-full px-4 py-2 animate-scale-in"> <div class="mb-glass rounded-full px-6 py-3 animate-scale-in">
<span id="live-time" class="text-sm font-semibold text-professional-primary"></span> <span id="live-time" class="text-sm font-semibold text-professional-primary"></span>
</div> </div>
</div> </div>
<div class="relative max-w-7xl mx-auto px-6 lg:px-8 py-16 z-10"> <div class="relative max-w-7xl mx-auto px-6 lg:px-8 py-20 z-10">
<div class="text-center animate-slide-up"> <div class="text-center animate-slide-up">
<!-- Mercedes-Benz Logo --> <!-- Mercedes-Benz Logo -->
<div class="inline-flex items-center justify-center w-24 h-24 mb-glass rounded-full mb-8 professional-shadow"> <div class="inline-flex items-center justify-center w-28 h-28 mb-glass rounded-full mb-10 professional-shadow">
<svg class="w-12 h-12 text-professional-primary" viewBox="0 0 80 80" fill="currentColor"> <svg class="w-14 h-14 text-professional-primary" viewBox="0 0 80 80" fill="currentColor">
<path d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5 <path d="M58.6,4.5C53,1.6,46.7,0,40,0c-6.7,0-13,1.6-18.6,4.5v0C8.7,11.2,0,24.6,0,40c0,15.4,8.7,28.8,21.5,35.5
C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40 C27,78.3,33.3,80,40,80c6.7,0,12.9-1.7,18.5-4.6C71.3,68.8,80,55.4,80,40C80,24.6,71.3,11.2,58.6,4.5z M4,40
c0-13.1,7-24.5,17.5-30.9v0C26.6,6,32.5,4.2,39,4l-4.5,32.7L21.5,46.8v0L8.3,57.1C5.6,52,4,46.2,4,40z M58.6,70.8 c0-13.1,7-24.5,17.5-30.9v0C26.6,6,32.5,4.2,39,4l-4.5,32.7L21.5,46.8v0L8.3,57.1C5.6,52,4,46.2,4,40z M58.6,70.8
@ -36,68 +54,77 @@
</svg> </svg>
</div> </div>
<h1 class="title-professional text-5xl md:text-6xl font-bold mb-6 tracking-tight"> <h1 class="title-professional text-6xl md:text-7xl font-bold mb-8 tracking-tight">
3D-Drucker Management 3D-Drucker Management
</h1> </h1>
<p class="subtitle-professional text-xl md:text-2xl max-w-4xl mx-auto leading-relaxed mb-8"> <p class="subtitle-professional text-2xl md:text-3xl max-w-5xl mx-auto leading-relaxed mb-12">
Verwalten Sie Ihre 3D-Drucker mit höchster Präzision und Mercedes-Benz Qualitätsstandard Verwalten Sie Ihre 3D-Drucker mit höchster Präzision und Mercedes-Benz Qualitätsstandard
</p> </p>
<!-- Status Overview --> <!-- Status Overview -->
<div class="flex flex-wrap justify-center gap-6 mb-8"> <div class="flex flex-wrap justify-center gap-8 mb-12">
<div class="mb-glass rounded-2xl px-6 py-4 animate-scale-in"> <div class="mb-glass rounded-3xl px-8 py-6 animate-scale-in min-w-[200px]">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-4">
<div class="status-dot status-online"></div> <div class="status-dot status-online w-4 h-4"></div>
<span class="text-professional-primary font-semibold">Online: <span id="online-count" class="text-green-500 font-bold">-</span></span> <div class="text-left">
<div class="text-sm text-professional-muted uppercase tracking-wide">Online</div>
<div class="text-3xl font-bold text-green-500" id="online-count">-</div>
</div>
</div> </div>
</div> </div>
<div class="mb-glass rounded-2xl px-6 py-4 animate-scale-in"> <div class="mb-glass rounded-3xl px-8 py-6 animate-scale-in min-w-[200px]">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-4">
<div class="status-dot status-offline"></div> <div class="status-dot status-offline w-4 h-4"></div>
<span class="text-professional-primary font-semibold">Offline: <span id="offline-count" class="text-red-500 font-bold">-</span></span> <div class="text-left">
<div class="text-sm text-professional-muted uppercase tracking-wide">Offline</div>
<div class="text-3xl font-bold text-red-500" id="offline-count">-</div>
</div>
</div> </div>
</div> </div>
<div class="mb-glass rounded-2xl px-6 py-4 animate-scale-in"> <div class="mb-glass rounded-3xl px-8 py-6 animate-scale-in min-w-[200px]">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-4">
<svg class="w-5 h-5 text-professional-primary" fill="currentColor" viewBox="0 0 20 20"> <svg class="w-6 h-6 text-professional-accent" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/>
</svg> </svg>
<span class="text-professional-primary font-semibold">Gesamt: <span id="total-count" class="text-professional-accent font-bold">-</span></span> <div class="text-left">
<div class="text-sm text-professional-muted uppercase tracking-wide">Gesamt</div>
<div class="text-3xl font-bold text-professional-accent" id="total-count">-</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="flex flex-wrap justify-center gap-4"> <div class="flex flex-wrap justify-center gap-6">
<!-- Filter Buttons --> <!-- Filter Buttons -->
<div class="flex bg-white/10 dark:bg-black/20 backdrop-blur-sm rounded-2xl p-2 border border-white/20 dark:border-white/10"> <div class="flex bg-white/10 dark:bg-black/30 backdrop-blur-sm rounded-3xl p-3 border border-white/20 dark:border-white/10">
<button id="filter-all" class="filter-btn-mercedes active px-4 py-2 rounded-xl text-sm font-semibold transition-all duration-300"> <button id="filter-all" class="filter-btn-mercedes active px-6 py-3 rounded-2xl text-sm font-semibold transition-all duration-300">
Alle Alle Drucker
</button> </button>
<button id="filter-online" class="filter-btn-mercedes px-4 py-2 rounded-xl text-sm font-semibold transition-all duration-300"> <button id="filter-online" class="filter-btn-mercedes px-6 py-3 rounded-2xl text-sm font-semibold transition-all duration-300">
Online Online
</button> </button>
<button id="filter-offline" class="filter-btn-mercedes px-4 py-2 rounded-xl text-sm font-semibold transition-all duration-300"> <button id="filter-offline" class="filter-btn-mercedes px-6 py-3 rounded-2xl text-sm font-semibold transition-all duration-300">
Offline Offline
</button> </button>
</div> </div>
<button onclick="toggleAutoRefresh()" id="auto-refresh-btn" class="btn-professional group"> <button onclick="toggleAutoRefresh()" id="auto-refresh-btn" class="btn-professional group px-8 py-4">
<svg class="w-6 h-6 mr-3 group-hover:rotate-180 transition-transform duration-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6 mr-3 group-hover:rotate-180 transition-transform duration-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg> </svg>
<span>Auto-Update</span> <span>Auto-Update</span>
</button> </button>
<button onclick="refreshPrinters()" class="btn-professional group"> <button onclick="refreshPrinters()" class="btn-professional group px-8 py-4">
<svg class="w-6 h-6 mr-3 group-hover:rotate-180 transition-transform duration-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6 mr-3 group-hover:rotate-180 transition-transform duration-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg> </svg>
<span>Jetzt aktualisieren</span> <span>Status aktualisieren</span>
</button> </button>
{% if current_user.is_admin %} {% if current_user.is_admin %}
<button id="addPrinterBtn" class="btn-success-professional group"> <button id="addPrinterBtn" class="btn-success-professional group px-8 py-4">
<svg class="w-6 h-6 mr-3 group-hover:scale-110 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-6 h-6 mr-3 group-hover:scale-110 transition-transform duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg> </svg>
@ -107,35 +134,35 @@
</div> </div>
<!-- Auto-Update Info --> <!-- Auto-Update Info -->
<div class="mt-6 flex items-center justify-center space-x-2 text-professional-muted"> <div class="mt-8 flex items-center justify-center space-x-3 text-professional-muted">
<svg id="auto-refresh-icon" class="w-4 h-4 animate-spin-slow" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg id="auto-refresh-icon" class="w-5 h-5 animate-spin-slow" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg> </svg>
<span class="text-sm">Auto-Update: <span id="next-update-time" class="font-mono">-</span>s</span> <span class="text-base">Auto-Update: <span id="next-update-time" class="font-mono font-bold">-</span>s</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 -mt-8 relative z-10 space-y-8"> <div class="max-w-7xl mx-auto px-6 lg:px-8 -mt-12 relative z-10" style="margin-bottom: 4rem;">
<!-- Printers Grid --> <!-- Printers Grid -->
<div class="professional-container p-8 lg:p-12 animate-slide-up"> <div class="professional-container animate-slide-up" style="padding: 3rem; border-radius: 2rem; margin-bottom: 2rem;">
<div class="text-center mb-10"> <div class="text-center mb-16">
<h2 class="title-professional text-3xl font-bold mb-4"> <h2 class="title-professional text-4xl font-bold mb-6">
Verfügbare Drucker Verfügbare 3D-Drucker
</h2> </h2>
<p class="subtitle-professional text-lg"> <p class="subtitle-professional text-xl max-w-3xl mx-auto">
Übersicht und Verwaltung Ihrer 3D-Drucker-Infrastruktur Übersicht und Verwaltung Ihrer gesamten 3D-Drucker-Infrastruktur mit Echtzeit-Statusanzeige
</p> </p>
</div> </div>
<div id="printers-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div id="printers-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<!-- Loading state --> <!-- Loading state -->
<div class="col-span-full text-center py-12"> <div class="col-span-full text-center py-16">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-professional-accent mx-auto"></div> <div class="animate-spin rounded-full h-16 w-16 border-b-3 border-professional-accent mx-auto mb-8"></div>
<p class="mt-4 text-base text-professional-secondary">Lade Drucker...</p> <h3 class="text-2xl font-bold text-professional-primary mb-4">Lade Drucker...</h3>
<p class="mt-1 text-sm text-professional-muted">Dies sollte nur wenige Sekunden dauern</p> <p class="text-lg text-professional-secondary">Dies sollte nur wenige Sekunden dauern</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,238 @@
#!/usr/bin/env python3
"""
Rate Limiting System für MYP Platform
Schutz vor API-Missbrauch und DDoS-Attacken
"""
import time
import redis
import hashlib
from functools import wraps
from flask import request, jsonify, g
from typing import Dict, Optional
from dataclasses import dataclass
from utils.logging_config import get_logger
logger = get_logger("security")
@dataclass
class RateLimit:
"""Konfiguration für Rate-Limiting-Regeln"""
requests: int # Anzahl erlaubter Anfragen
per: int # Zeitraum in Sekunden
message: str # Fehlermeldung bei Überschreitung
# Rate-Limiting-Konfiguration
RATE_LIMITS = {
# API-Endpunkte
'api_general': RateLimit(100, 300, "Zu viele API-Anfragen. Versuchen Sie es in 5 Minuten erneut."),
'api_auth': RateLimit(10, 300, "Zu viele Anmeldeversuche. Versuchen Sie es in 5 Minuten erneut."),
'api_upload': RateLimit(20, 3600, "Zu viele Upload-Anfragen. Versuchen Sie es in einer Stunde erneut."),
'api_admin': RateLimit(200, 300, "Zu viele Admin-Anfragen. Versuchen Sie es in 5 Minuten erneut."),
# Spezielle Endpunkte
'printer_status': RateLimit(300, 300, "Zu viele Drucker-Status-Anfragen."),
'job_creation': RateLimit(50, 3600, "Zu viele Job-Erstellungen. Versuchen Sie es in einer Stunde erneut."),
# Sicherheitskritische Endpunkte
'password_reset': RateLimit(3, 3600, "Zu viele Passwort-Reset-Anfragen. Versuchen Sie es in einer Stunde erneut."),
'user_creation': RateLimit(10, 3600, "Zu viele Benutzer-Erstellungen.")
}
class RateLimiter:
"""
In-Memory Rate Limiter mit optionaler Redis-Unterstützung
"""
def __init__(self, use_redis: bool = False, redis_url: str = None):
self.use_redis = use_redis
self.redis_client = None
self.memory_store: Dict[str, Dict] = {}
if use_redis and redis_url:
try:
import redis
self.redis_client = redis.from_url(redis_url, decode_responses=True)
logger.info("✅ Redis-basiertes Rate Limiting aktiviert")
except ImportError:
logger.warning("⚠️ Redis nicht verfügbar, verwende In-Memory Rate Limiting")
self.use_redis = False
except Exception as e:
logger.error(f"❌ Redis-Verbindung fehlgeschlagen: {e}")
self.use_redis = False
def _get_client_id(self) -> str:
"""
Generiert eine eindeutige Client-ID basierend auf IP und User-Agent
"""
ip = request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr)
user_agent = request.headers.get('User-Agent', '')
# Hash für Anonymisierung
client_string = f"{ip}:{user_agent}"
return hashlib.sha256(client_string.encode()).hexdigest()[:16]
def _get_key(self, limit_type: str, client_id: str) -> str:
"""Erstellt Redis/Memory-Key für Rate-Limiting"""
return f"rate_limit:{limit_type}:{client_id}"
def _get_current_requests(self, key: str, window_start: int) -> int:
"""Holt aktuelle Anfragen-Anzahl"""
if self.use_redis and self.redis_client:
try:
# Redis-basierte Implementierung
pipe = self.redis_client.pipeline()
pipe.zremrangebyscore(key, 0, window_start)
pipe.zcard(key)
_, count = pipe.execute()
return count
except Exception as e:
logger.error(f"Redis-Fehler: {e}, fallback zu Memory")
self.use_redis = False
# In-Memory Implementierung
if key not in self.memory_store:
self.memory_store[key] = {'requests': [], 'last_cleanup': time.time()}
# Alte Einträge bereinigen
current_time = time.time()
data = self.memory_store[key]
data['requests'] = [req_time for req_time in data['requests'] if req_time > window_start]
return len(data['requests'])
def _add_request(self, key: str, current_time: int, expire_time: int):
"""Fügt neue Anfrage hinzu"""
if self.use_redis and self.redis_client:
try:
pipe = self.redis_client.pipeline()
pipe.zadd(key, {str(current_time): current_time})
pipe.expire(key, expire_time)
pipe.execute()
return
except Exception as e:
logger.error(f"Redis-Fehler: {e}, fallback zu Memory")
self.use_redis = False
# In-Memory Implementierung
if key not in self.memory_store:
self.memory_store[key] = {'requests': [], 'last_cleanup': time.time()}
self.memory_store[key]['requests'].append(current_time)
def is_allowed(self, limit_type: str) -> tuple[bool, Dict]:
"""
Prüft ob eine Anfrage erlaubt ist
Returns:
(is_allowed, info_dict)
"""
if limit_type not in RATE_LIMITS:
return True, {}
rate_limit = RATE_LIMITS[limit_type]
client_id = self._get_client_id()
key = self._get_key(limit_type, client_id)
current_time = int(time.time())
window_start = current_time - rate_limit.per
# Aktuelle Anfragen zählen
current_requests = self._get_current_requests(key, window_start)
# Limite prüfen
if current_requests >= rate_limit.requests:
logger.warning(f"🚨 Rate limit exceeded: {limit_type} für Client {client_id[:8]}...")
return False, {
'limit': rate_limit.requests,
'remaining': 0,
'reset_time': current_time + rate_limit.per,
'message': rate_limit.message
}
# Anfrage hinzufügen
self._add_request(key, current_time, rate_limit.per)
return True, {
'limit': rate_limit.requests,
'remaining': rate_limit.requests - current_requests - 1,
'reset_time': current_time + rate_limit.per
}
def cleanup_memory(self):
"""Bereinigt alte In-Memory-Einträge"""
if self.use_redis:
return
current_time = time.time()
keys_to_delete = []
for key, data in self.memory_store.items():
# Bereinige alle Einträge älter als 24 Stunden
if current_time - data.get('last_cleanup', 0) > 86400:
keys_to_delete.append(key)
for key in keys_to_delete:
del self.memory_store[key]
# Globale Rate-Limiter-Instanz
rate_limiter = RateLimiter()
def limit_requests(limit_type: str):
"""
Decorator für Rate-Limiting von API-Endpunkten
Args:
limit_type: Art des Limits (siehe RATE_LIMITS)
"""
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
# Rate-Limiting prüfen
is_allowed, info = rate_limiter.is_allowed(limit_type)
if not is_allowed:
response = jsonify({
'error': 'Rate limit exceeded',
'message': info['message'],
'retry_after': info['reset_time'] - int(time.time())
})
response.status_code = 429
response.headers['Retry-After'] = str(info['reset_time'] - int(time.time()))
response.headers['X-RateLimit-Limit'] = str(info['limit'])
response.headers['X-RateLimit-Remaining'] = str(info['remaining'])
response.headers['X-RateLimit-Reset'] = str(info['reset_time'])
return response
# Rate-Limiting-Headers zu Response hinzufügen
response = f(*args, **kwargs)
if hasattr(response, 'headers'):
response.headers['X-RateLimit-Limit'] = str(info['limit'])
response.headers['X-RateLimit-Remaining'] = str(info['remaining'])
response.headers['X-RateLimit-Reset'] = str(info['reset_time'])
return response
return wrapper
return decorator
def get_client_info() -> Dict:
"""
Gibt Client-Informationen für Rate-Limiting zurück
"""
client_id = rate_limiter._get_client_id()
ip = request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr)
return {
'client_id': client_id,
'ip_address': ip,
'user_agent': request.headers.get('User-Agent', ''),
'timestamp': int(time.time())
}
# Maintenance-Task für Memory-Cleanup
def cleanup_rate_limiter():
"""Periodische Bereinigung des Rate-Limiters"""
rate_limiter.cleanup_memory()
logger.debug("🧹 Rate-Limiter Memory bereinigt")

View File

@ -0,0 +1,328 @@
#!/usr/bin/env python3
"""
Security Utilities für MYP Platform
Content Security Policy (CSP), Security Headers und weitere Sicherheitsmaßnahmen
"""
import secrets
import hashlib
from flask import request, g, session
from functools import wraps
from typing import Dict, List, Optional
from utils.logging_config import get_logger
logger = get_logger("security")
# Content Security Policy Konfiguration
CSP_POLICY = {
'default-src': ["'self'"],
'script-src': [
"'self'",
"'unsafe-inline'", # Temporär für Dark Mode Script
"https://cdn.jsdelivr.net", # Für externe Libraries
"https://unpkg.com" # Für Fallback-Libraries
],
'style-src': [
"'self'",
"'unsafe-inline'", # Für Tailwind und Dynamic Styles
"https://fonts.googleapis.com"
],
'img-src': [
"'self'",
"data:", # Für SVG Data URLs
"blob:", # Für dynamisch generierte Bilder
"https:" # HTTPS-Bilder erlauben
],
'font-src': [
"'self'",
"https://fonts.gstatic.com",
"data:" # Für eingebettete Fonts
],
'connect-src': [
"'self'",
"ws:", # WebSocket für lokale Entwicklung
"wss:" # Sichere WebSockets
],
'media-src': ["'self'"],
'object-src': ["'none'"], # Flash und andere Plugins blockieren
'base-uri': ["'self'"],
'form-action': ["'self'"],
'frame-ancestors': ["'none'"], # Clickjacking-Schutz
'upgrade-insecure-requests': True,
'block-all-mixed-content': True
}
# Security Headers Konfiguration
SECURITY_HEADERS = {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Permissions-Policy': (
'geolocation=(), '
'microphone=(), '
'camera=(), '
'payment=(), '
'usb=(), '
'accelerometer=(), '
'gyroscope=(), '
'magnetometer=()'
),
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Resource-Policy': 'same-origin'
}
class SecurityManager:
"""
Zentrale Sicherheitsverwaltung für MYP Platform
"""
def __init__(self):
self.nonce_store: Dict[str, str] = {}
def generate_nonce(self) -> str:
"""Generiert eine sichere Nonce für CSP"""
nonce = secrets.token_urlsafe(32)
# Nonce in Session speichern für Validierung
if 'security_nonces' not in session:
session['security_nonces'] = []
session['security_nonces'].append(nonce)
# Maximal 10 Nonces pro Session
if len(session['security_nonces']) > 10:
session['security_nonces'] = session['security_nonces'][-10:]
return nonce
def validate_nonce(self, nonce: str) -> bool:
"""Validiert eine Nonce"""
if 'security_nonces' not in session:
return False
return nonce in session['security_nonces']
def build_csp_header(self, nonce: Optional[str] = None) -> str:
"""
Erstellt den Content-Security-Policy Header
Args:
nonce: Optional CSP nonce für inline scripts
Returns:
CSP Header String
"""
csp_parts = []
for directive, values in CSP_POLICY.items():
if directive in ['upgrade-insecure-requests', 'block-all-mixed-content']:
if values:
csp_parts.append(directive.replace('_', '-'))
continue
if isinstance(values, list):
directive_values = values.copy()
# Nonce für script-src hinzufügen
if directive == 'script-src' and nonce:
directive_values.append(f"'nonce-{nonce}'")
csp_parts.append(f"{directive.replace('_', '-')} {' '.join(directive_values)}")
return "; ".join(csp_parts)
def get_client_fingerprint(self) -> str:
"""
Erstellt einen Client-Fingerprint für erweiterte Sicherheit
"""
components = [
request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr),
request.headers.get('User-Agent', ''),
request.headers.get('Accept-Language', ''),
request.headers.get('Accept-Encoding', '')
]
fingerprint_string = '|'.join(components)
return hashlib.sha256(fingerprint_string.encode()).hexdigest()[:32]
def check_suspicious_activity(self) -> bool:
"""
Prüft auf verdächtige Aktivitäten
"""
# SQL Injection Patterns
sql_patterns = [
'union select', 'drop table', 'insert into', 'delete from',
'script>', '<iframe', 'javascript:', 'vbscript:',
'onload=', 'onerror=', 'onclick='
]
# Request-Daten prüfen
request_data = str(request.args) + str(request.form) + str(request.json or {})
request_data_lower = request_data.lower()
for pattern in sql_patterns:
if pattern in request_data_lower:
logger.warning(f"🚨 Verdächtige Aktivität erkannt: {pattern} von {request.remote_addr}")
return True
# Übermäßig große Requests
if len(request_data) > 50000: # 50KB Limit
logger.warning(f"🚨 Übermäßig große Anfrage von {request.remote_addr}: {len(request_data)} bytes")
return True
return False
def log_security_event(self, event_type: str, details: Dict):
"""
Protokolliert Sicherheitsereignisse
"""
security_data = {
'event_type': event_type,
'ip_address': request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr),
'user_agent': request.headers.get('User-Agent', ''),
'timestamp': request.environ.get('REQUEST_START_TIME'),
'fingerprint': self.get_client_fingerprint(),
**details
}
logger.warning(f"🔒 Sicherheitsereignis: {event_type} - {security_data}")
# Globale Security Manager Instanz
security_manager = SecurityManager()
def apply_security_headers(response):
"""
Wendet Sicherheits-Headers auf Response an
"""
# Standard Security Headers
for header, value in SECURITY_HEADERS.items():
response.headers[header] = value
# Content Security Policy
nonce = getattr(g, 'csp_nonce', None)
csp_header = security_manager.build_csp_header(nonce)
response.headers['Content-Security-Policy'] = csp_header
# HSTS für HTTPS
if request.is_secure:
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains; preload'
return response
def security_check(check_suspicious: bool = True):
"""
Decorator für Sicherheitsprüfungen
Args:
check_suspicious: Ob auf verdächtige Aktivitäten geprüft werden soll
"""
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
# Verdächtige Aktivitäten prüfen
if check_suspicious and security_manager.check_suspicious_activity():
security_manager.log_security_event('suspicious_request', {
'endpoint': request.endpoint,
'method': request.method,
'args': dict(request.args),
'form': dict(request.form)
})
from flask import jsonify
return jsonify({
'error': 'Verdächtige Anfrage erkannt',
'message': 'Ihre Anfrage wurde aus Sicherheitsgründen blockiert.'
}), 400
return f(*args, **kwargs)
return wrapper
return decorator
def require_secure_headers(f):
"""
Decorator der sicherstellt, dass Security Headers gesetzt werden
"""
@wraps(f)
def wrapper(*args, **kwargs):
# CSP Nonce generieren
g.csp_nonce = security_manager.generate_nonce()
response = f(*args, **kwargs)
# Security Headers anwenden
if hasattr(response, 'headers'):
response = apply_security_headers(response)
return response
return wrapper
def get_csp_nonce() -> str:
"""
Holt die aktuelle CSP Nonce für Templates
"""
return getattr(g, 'csp_nonce', '')
def validate_origin():
"""
Validiert die Origin des Requests
"""
origin = request.headers.get('Origin')
referer = request.headers.get('Referer')
host = request.headers.get('Host')
# Für API-Requests Origin prüfen
if request.path.startswith('/api/') and origin:
allowed_origins = [
f"http://{host}",
f"https://{host}",
"http://localhost:5000",
"http://127.0.0.1:5000"
]
if origin not in allowed_origins:
logger.warning(f"🚨 Ungültige Origin: {origin} für {request.path}")
return False
return True
# Template Helper für CSP Nonce
def csp_nonce():
"""Template Helper für CSP Nonce"""
return get_csp_nonce()
# Security Middleware für Flask App
def init_security(app):
"""
Initialisiert Sicherheitsfeatures für Flask App
"""
@app.before_request
def before_request_security():
"""Security Checks vor jedem Request"""
# Origin validieren
if not validate_origin():
from flask import jsonify
return jsonify({
'error': 'Invalid origin',
'message': 'Anfrage von ungültiger Quelle'
}), 403
# CSP Nonce generieren
g.csp_nonce = security_manager.generate_nonce()
@app.after_request
def after_request_security(response):
"""Security Headers nach jedem Request anwenden"""
return apply_security_headers(response)
# Template Helper registrieren
app.jinja_env.globals['csp_nonce'] = csp_nonce
logger.info("🔒 Security System initialisiert")
return app