""" Validateurs de sécurité pour prévenir les injections et attaques """ import re from pathlib import Path from typing import Optional from fastapi import HTTPException, status import logging logger = logging.getLogger("innotexboard.security") def validate_container_name(name: str) -> str: """ Valide un nom de conteneur Docker Protection contre injection de commandes """ if not name or len(name) > 255: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Nom de conteneur invalide" ) # Docker accepte: lettres, chiffres, tirets, underscores, points pattern = re.compile(r'^[a-zA-Z0-9][a-zA-Z0-9_.-]*$') if not pattern.match(name): logger.warning(f"Invalid container name attempted: {name}") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Nom de conteneur contient des caractères invalides" ) return name def validate_image_name(image: str) -> str: """ Valide un nom d'image Docker Format: [registry/]repository[:tag] """ if not image or len(image) > 512: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Nom d'image invalide" ) # Pattern pour image Docker valide pattern = re.compile( r'^(?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)*' r'[a-z0-9]+(?:[._-][a-z0-9]+)*' r'(?::[a-zA-Z0-9][a-zA-Z0-9._-]*)?$' ) if not pattern.match(image): logger.warning(f"Invalid image name attempted: {image}") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Nom d'image contient des caractères invalides" ) return image def validate_file_path(file_path: str, allowed_base: str = "/home/innotex/Docker") -> Path: """ Valide un chemin de fichier et prévient le path traversal Vérifie que le chemin reste dans le répertoire autorisé """ try: # Convertir en Path absolu et résoudre les liens symboliques target_path = Path(file_path).resolve() base_path = Path(allowed_base).resolve() # Vérifier que le chemin est dans le répertoire autorisé if not str(target_path).startswith(str(base_path)): logger.warning(f"Path traversal attempt detected: {file_path}") raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Accès au chemin non autorisé" ) return target_path except (ValueError, RuntimeError) as e: logger.warning(f"Invalid path attempted: {file_path} - {e}") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Chemin de fichier invalide" ) def validate_compose_name(name: str) -> str: """ Valide un nom de fichier docker-compose Accepte uniquement les noms de fichiers sûrs """ if not name or len(name) > 100: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Nom de compose invalide" ) # Accepter uniquement alphanumériques, tirets, underscores pattern = re.compile(r'^[a-zA-Z0-9_-]+$') if not pattern.match(name): logger.warning(f"Invalid compose name attempted: {name}") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Nom de compose contient des caractères invalides" ) return name def validate_package_name(package: str) -> str: """ Valide un nom de package Debian/APT Protection contre injection de commandes """ if not package or len(package) > 200: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Nom de package invalide" ) # Pattern pour nom de package Debian valide pattern = re.compile(r'^[a-z0-9][a-z0-9+.-]*$') if not pattern.match(package): logger.warning(f"Invalid package name attempted: {package}") raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Nom de package contient des caractères invalides" ) return package def sanitize_command_output(output: str, max_length: int = 10000) -> str: """ Nettoie la sortie de commande pour éviter l'injection de contenu malveillant Limite la taille pour prévenir DoS """ if not output: return "" # Limiter la taille if len(output) > max_length: output = output[:max_length] + "\n[...truncated...]" # Retirer les caractères de contrôle dangereux (sauf newline et tab) sanitized = re.sub(r'[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]', '', output) return sanitized def validate_port(port: int) -> int: """ Valide un numéro de port """ if not isinstance(port, int) or port < 1 or port > 65535: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Port invalide: {port}. Doit être entre 1 et 65535" ) # Ports privilégiés (< 1024) nécessitent root if port < 1024: logger.warning(f"Privileged port requested: {port}") return port def validate_environment_variable(key: str, value: str) -> tuple[str, str]: """ Valide une variable d'environnement Protection contre injection """ # Clé: lettres, chiffres, underscores uniquement key_pattern = re.compile(r'^[A-Z_][A-Z0-9_]*$') if not key_pattern.match(key): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Nom de variable d'environnement invalide: {key}" ) # Valeur: pas de null bytes if '\x00' in value: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Valeur de variable d'environnement contient des caractères invalides" ) # Limiter la taille if len(value) > 10000: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Valeur de variable d'environnement trop longue" ) return key, value