protection de l'application contre les attaques numériques
This commit is contained in:
200
backend/app/core/validators.py
Normal file
200
backend/app/core/validators.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user