From 9c89ad1397b4218c4645f8abfd7a9f1a8c189f90 Mon Sep 17 00:00:00 2001 From: Till Tomczak Date: Thu, 29 May 2025 15:38:32 +0200 Subject: [PATCH] "feat: Integrate rate limiter --- backend/app/database/myp.db | Bin 126976 -> 126976 bytes backend/app/database/myp.db-wal | Bin 98912 -> 4152 bytes backend/app/templates/printers.html | 129 ++++++----- backend/app/utils/rate_limiter.py | 238 ++++++++++++++++++++ backend/app/utils/security.py | 328 ++++++++++++++++++++++++++++ 5 files changed, 644 insertions(+), 51 deletions(-) create mode 100644 backend/app/utils/rate_limiter.py create mode 100644 backend/app/utils/security.py diff --git a/backend/app/database/myp.db b/backend/app/database/myp.db index c39593dabc6fec325500b6926d8b29c9071ba948..20b582d332eb5e64e94f6a4298a69e81a8593af5 100644 GIT binary patch delta 533 zcmZ9G&r1S99K|z`2*NBVkI5XYplp9M;@Zh1pQn|Oe^g=2#JWtF|Y-NUL@iRy+ zXQs>Ldl>C4t-yMCynkJ0u+ubjwX$8P)HW)+M|I`6O*Y+XeuteHqF~fev7@|6!wrygpB{Y5w#X`+sNw1qsZcJ z+vQ!8(95wA-l3)=7-c_2(L*7pGDkystFFi`A`bt)!4@Xa6b-H`t8iU2PE}aa3nf^{ zE1IGf3~=)WCCjS%7kkaVug&VklBVUgq6#~1OtNr0}wD9vP!<#b?o2N0}LE40?(E$28wY3u{=yXJn_}+ M4VS+=EVK{+0G6#5+W-In literal 98912 zcmeI*Z)_W99S3kHwL?-8yQLH}O37hGL>u^W&vXBL7p<$Er1`r#X|lP^{zQp=NiOl9 z=6udSX&_H@6V-|YllE!|c!78Wf-)E{5PPu~>;;;HgwQA;Awk3tVxkNcBs5eWJ0#q- zr_O4KaN@7qB+he}%kMspAL;q(o_o~KwLI&K{_dkb-!UJ1G?TysZ>KUNTd(%WDjL>|nUOj9 zp$>ny_I|rt#%yKZlkNVfFI4^6%Z8ORi^a;(N!JJc;fGIDnv2DitZ5rFvn9J=<}=26 z^%oUq2;oTA(f8Ynvb{dl#`X$Vk5#F<`lR|fKbtdVjNGi5WqWbtRI5My>FwY7-~Dzr zf8qoFFu8MMZkb#6Nw zW{AR0?}-1j7~W~B)U@+_rgE@SQQ_FXNHg1O_lT~&X4AI0YOsCyCjWzvoLDFsMSI3F zRw`esub{i-KSzymG4=->U?01=z{aicyO*x4BrCZ4$^Oa& z>MWAfTBNFo_mXV#E|Sz(q~I))up)`GNTJpuW$}(T4i$Au*c*qaio`lKS1$1S)z^K~ z&m}*DT%bX_dz>}|AOHafKmY;|fB*zM1&|960l9$1As4titGX)}a1Oc4kpq|u1mAio zmReg9;!QKrX@63VN}pSl3qv$9nx4Lt$PR9d_Vk}C zEiaKCdu_hYxSX0!8;gsBONmtfc*0no%ndzm4enDeAQN_4u3TVq^ZIGM{ky+JF5q~< z@e~3OfB*y_009U<00Izz00bc55ZGHTFt;R*iiPQTVRJGmiam=%bE#Y`xiFAO&i9ng z&#N0GwXnLj@#yf{Qfg&%Z8fzK&l&rX3$SYpM9NOfl?xP;A3pi~U!E!>7jV4bcnSds zKmY;|fB*y_009U<00Izj2<$NzAc>VJd*M>T9*R-RwENi<0;4oBwXWDy-Wa1~BqLnx zORtL7WBi32AqhLSu(D6NfK-`B5MUp>_ZRrpo3~%?{Cx6SB^PLEkN84Q1YZpv@5r{l z!M?!*0uX=z1Rwwb2tWV=5NJ>V+;b!T+wY54qzWS*!KhF+e>`r=DI4bBw)n$oSi4bG$0+pkJ z9HnfcysYECg933%-v50EMWPddr>Z7uBJ0##xxkOlztYw>y!94xfd=jFaoP}o00bZa z0SG_<0ub;NKrVn>0J*?UMBxrER7G@$7tSGfIZ~(V6grD!Z+TZP@Xl>*df*>hS>ytq zogo?_009U<00Izz00bb=paRGRkPFm9F5n!^mLqk#PNBQV1DFf^@n!KBxi5aU1Gzwh zcK0}K2tWV=5P$##AOHafcnTmFIP|%Ido+6_=<7V$+uhS04xeg`$Hy%*k6fTpt{3>m zKmVTp-cP=L9l3yKXNX1!KmY;|fB*y_009Uzr~q<-z2pM!A-CUq$Su`4bzgK&-Iv&s z^^$YKxmfemeMPy~)$hm!NUf5GZyv#!-#`3?iR+*2MJ~{w-91hl0uX=z1Rwwb2tWV= zo&t573%G~e0q-HVT;q^iat^s=HsqF_LvE?ofueTj-SXT;A{U^wO5SzfLANC45$w-> z2Rqbvjvjk{^2^8tJUc@)LI45~fB*y_009UY0x}iCn<5Gejc`Uz zuzFpOz*Qu2fjz88;F84k0()4Gz#+-r^3Cj{lLQ`kJCzyPx;ZBEN1l1_S|t}~3ibNf ze>@-n0SG_<0uX=z1Rwwb2tWV=5O4^ThntT3dc)QArj2 +
+ + -
-
+
+
-
-
-
+
+
+
- Live + Live Status
-
+
-
+
-
- +
+ +

3D-Drucker Management

-

+

Verwalten Sie Ihre 3D-Drucker mit höchster Präzision und Mercedes-Benz Qualitätsstandard

-
-
-
-
- Online: - +
+
+
+
+
+
Online
+
-
+
-
-
-
- Offline: - +
+
+
+
+
Offline
+
-
+
-
-
- +
+
+ - Gesamt: - +
+
Gesamt
+
-
+
-
+
-
- - -
- - {% if current_user.is_admin %} -
-
- +
+ - Auto-Update: -s + Auto-Update: -s
-
+
-
-
-

- Verfügbare Drucker +
+
+

+ Verfügbare 3D-Drucker

-

- Übersicht und Verwaltung Ihrer 3D-Drucker-Infrastruktur +

+ Übersicht und Verwaltung Ihrer gesamten 3D-Drucker-Infrastruktur mit Echtzeit-Statusanzeige

-
+
-
-
-

Lade Drucker...

-

Dies sollte nur wenige Sekunden dauern

+
+
+

Lade Drucker...

+

Dies sollte nur wenige Sekunden dauern

diff --git a/backend/app/utils/rate_limiter.py b/backend/app/utils/rate_limiter.py new file mode 100644 index 00000000..3f3fbe64 --- /dev/null +++ b/backend/app/utils/rate_limiter.py @@ -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") \ No newline at end of file diff --git a/backend/app/utils/security.py b/backend/app/utils/security.py new file mode 100644 index 00000000..c8033688 --- /dev/null +++ b/backend/app/utils/security.py @@ -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>', ' 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 \ No newline at end of file