201 lines
6.2 KiB
Python
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
|