feat: sécurité HIGH — token Bearer, validation, rate limiting, headers
- Token Bearer auth sur le streaming server (auto-généré ou env var) - Validation actions replay (types, longueurs, coordonnées 0-1) - Rate limiting in-memory (10 replays/min, 200 images/min) - Security headers Flask (nosniff, SAMEORIGIN, XSS) - Validation uploads (50MB max, MIME type) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -79,8 +79,22 @@ logger = logging.getLogger(__name__)
|
||||
app = Flask(__name__)
|
||||
import secrets as _secrets
|
||||
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', _secrets.token_hex(32))
|
||||
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50 MB max upload (sécurité HIGH)
|
||||
socketio = SocketIO(app, cors_allowed_origins="*")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Headers de sécurité (sécurité HIGH)
|
||||
# ============================================================
|
||||
@app.after_request
|
||||
def set_security_headers(response):
|
||||
"""Ajouter les headers de sécurité à toutes les réponses."""
|
||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
|
||||
response.headers['X-XSS-Protection'] = '1; mode=block'
|
||||
return response
|
||||
|
||||
|
||||
# Global state
|
||||
matcher: Optional[SemanticMatcher] = None
|
||||
gpu_manager = None
|
||||
@@ -1365,6 +1379,11 @@ def api_chat_upload():
|
||||
Upload d'un fichier Excel/CSV via le chat.
|
||||
|
||||
Le fichier est stocké dans data/uploads/ et un aperçu est retourné.
|
||||
Validations de sécurité (HIGH) :
|
||||
- Taille max : 50 MB (via MAX_CONTENT_LENGTH Flask)
|
||||
- Extension autorisée : .xlsx, .xls, .csv
|
||||
- Type MIME vérifié (pas juste l'extension)
|
||||
- Nom de fichier assaini via secure_filename
|
||||
"""
|
||||
if 'file' not in request.files:
|
||||
return jsonify({"error": "Aucun fichier reçu."}), 400
|
||||
@@ -1381,9 +1400,32 @@ def api_chat_upload():
|
||||
"error": f"Format '{ext}' non supporté. Formats acceptés : {', '.join(allowed_ext)}"
|
||||
}), 400
|
||||
|
||||
# Sauvegarder le fichier
|
||||
# Validation du type MIME (sécurité HIGH — pas juste l'extension)
|
||||
_ALLOWED_MIMES = {
|
||||
'.xlsx': {'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/octet-stream'},
|
||||
'.xls': {'application/vnd.ms-excel', 'application/octet-stream'},
|
||||
'.csv': {'text/csv', 'text/plain', 'application/csv', 'application/octet-stream'},
|
||||
}
|
||||
file_mime = file.content_type or ''
|
||||
allowed_mimes = _ALLOWED_MIMES.get(ext, set())
|
||||
if file_mime and file_mime not in allowed_mimes:
|
||||
logger.warning(
|
||||
f"Upload rejeté : MIME '{file_mime}' invalide pour extension '{ext}' "
|
||||
f"(fichier: {file.filename})"
|
||||
)
|
||||
return jsonify({
|
||||
"error": f"Type MIME '{file_mime}' invalide pour un fichier {ext}. "
|
||||
f"Types attendus : {', '.join(allowed_mimes)}"
|
||||
}), 400
|
||||
|
||||
# Assainir le nom de fichier (sécurité HIGH — empêcher path traversal)
|
||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||
filename = secure_filename(file.filename)
|
||||
if not filename:
|
||||
# secure_filename peut retourner une chaîne vide pour des noms exotiques
|
||||
filename = f"upload_{datetime.now().strftime('%Y%m%d_%H%M%S')}{ext}"
|
||||
|
||||
# Ajouter un timestamp pour éviter les collisions
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
safe_name = f"{Path(filename).stem}_{ts}{ext}"
|
||||
|
||||
Reference in New Issue
Block a user