Files
innotexBoard/backend/app/core/validators.py

201 lines
6.2 KiB
Python

"""
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