feat(security): eval()→AST parseur + pickle→JSON+HMAC signé

Vulnérabilité 1 — eval() dans DAG executor :
- Nouveau module safe_condition_evaluator.py
- Parseur AST avec whitelist (Constants, Names, Compare, BoolOp, BinOp)
- Rejet explicite Call/Lambda/Import/__dunder__/walrus/comprehensions
- Expression non sûre → logged ERROR + évaluée à False (pas de crash)
- 31 tests (12 valides, 17 malveillantes rejetées, 2 intégration)

Vulnérabilité 2 — 3× pickle.load() non sécurisés :
- Nouveau module signed_serializer.py (JSON+HMAC-SHA256)
- Format : RPA_SIGNED_V1\\n + JSON(hmac + payload base64)
- Migration automatique transparente au premier chargement
- Fallback pickle avec WARNING (désactivable RPA_ALLOW_PICKLE_FALLBACK=0)
- Remplacement dans faiss_manager, visual_embedding_manager,
  visual_persistence_manager
- 13 tests

Clé signature : RPA_SIGNING_KEY (fallback TOKEN_SECRET_KEY puis hostname-derived).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dom
2026-04-14 16:49:17 +02:00
parent 93ef93e563
commit 36737cfe9d
8 changed files with 1110 additions and 50 deletions

View File

@@ -11,7 +11,12 @@ from pathlib import Path
from dataclasses import dataclass
import numpy as np
import json
import pickle
from core.security.signed_serializer import (
SignatureVerificationError,
load_signed,
save_signed,
)
logger = logging.getLogger(__name__)
@@ -500,21 +505,23 @@ class FAISSManager:
# Sauvegarder index FAISS
faiss.write_index(index_to_save, str(index_path))
# Sauvegarder métadonnées
# Sauvegarder métadonnées (JSON signé HMAC — cf. core.security.signed_serializer)
metadata = {
"dimensions": self.dimensions,
"index_type": self.index_type,
"metric": self.metric,
"next_id": self.next_id,
"metadata_store": self.metadata_store,
# Les clés dict sont des int côté Python ; on les sérialise en str
# puis on les reconvertit au chargement. JSON n'autorise pas de
# clés non-string.
"metadata_store": {str(k): v for k, v in self.metadata_store.items()},
"nlist": self.nlist,
"nprobe": self.nprobe,
"is_trained": self.is_trained,
"auto_optimize": self.auto_optimize
"auto_optimize": self.auto_optimize,
}
with open(metadata_path, 'wb') as f:
pickle.dump(metadata, f)
save_signed(metadata_path, metadata)
@classmethod
def load(cls, index_path: Path, metadata_path: Path, use_gpu: bool = False) -> 'FAISSManager':
@@ -529,11 +536,22 @@ class FAISSManager:
Returns:
FAISSManager chargé
"""
# Charger métadonnées
with open(metadata_path, 'rb') as f:
metadata = pickle.load(f)
# Créer instance
# Charger métadonnées (JSON signé ; fallback legacy pickle avec migration).
try:
metadata = load_signed(metadata_path)
except SignatureVerificationError:
logger.error(
"Signature HMAC invalide pour %s — refus de chargement.",
metadata_path,
)
raise
# Reconvertir les clés int du metadata_store (JSON force des clés str).
if isinstance(metadata.get("metadata_store"), dict):
metadata["metadata_store"] = {
int(k) if isinstance(k, str) and k.lstrip("-").isdigit() else k: v
for k, v in metadata["metadata_store"].items()
}
manager = cls(
dimensions=metadata["dimensions"],
index_type=metadata["index_type"],