Luka w Keras: ujawnienie danych i SSRF przy ładowaniu modeli (.keras) — CVE-2025-12058 - Security Bez Tabu

Luka w Keras: ujawnienie danych i SSRF przy ładowaniu modeli (.keras) — CVE-2025-12058

Wprowadzenie do problemu / definicja luki

W Keras (biblioteka DL) odkryto lukę CVE-2025-12058 umożliwiającą arBITRALNE ODCZYTY PLIKÓW (LFI) z systemu hosta podczas ładowania zserializowanych modeli .keras oraz SSRF (wykonywanie żądań sieciowych po stronie serwera). Problem dotyczy warstwy preprocessingowej StringLookup/IntegerLookup, która dopuszczała podanie ścieżki do pliku lub URL jako źródła słownika („vocabulary”). Podczas deserializacji modelu Keras odczytywał tę ścieżkę, nawet przy aktywnym safe_mode=True. Luka została naprawiona w Keras 3.11.4.


W skrócie

  • Identyfikator: CVE-2025-12058, CVSS 4.0: 5.9 (Medium) wg CNA (Google).
  • Wektory ataku: LFI przez odczyt lokalnych plików (np. klucze SSH), SSRF przez zdalne handlery tf.io.gfile (HTTP(S), GCS/HDFS).
  • Warunek: ofiara ładuje zewnętrzny, złośliwy model .keras (supply chain / model zoo / współdzielone repo).
  • Dotknięte wersje: Keras ≤ 3.11.3; naprawa w 3.11.4.
  • Naprawa: włączenie osadzania słownika w archiwum .keras oraz zablokowanie ładowania zewnętrznych słowników przy safe_mode=True.

Kontekst / historia / powiązania

Keras 3.x wprowadził ujednolicony interfejs (JAX/TensorFlow/PyTorch), a wraz z nim nowy format .keras do zapisu modeli. Od początku zakładano mechanizmy „bezpiecznego ładowania” (safe_mode), jednak w tym przypadku ochrona nie obejmowała konstrukcji warstw *Lookup z parametrem vocabulary wskazującym ścieżkę/URL. Problem zgłosił zespół Zscaler (ThreatLabz) — opisując realne scenariusze nadużyć oraz oś czasu ujawnienia. Poprawki trafiły do głównej gałęzi w październiku 2025 r., a wydanie naprawcze opublikowano jako 3.11.4.


Analiza techniczna / szczegóły luki

Gdzie leży błąd?

  • StringLookup i IntegerLookup pozwalały na vocabulary=<ścieżka lub URL>.
  • Gdy model był wczytywany z .keras, deserializacja rekonstruowała warstwę i odczytywała wskazaną ścieżkę/URL, włączając zawartość do stanu warstwy (np. dostępna przez get_vocabulary()), nie respektując w pełni safe_mode=True.

Skutki techniczne

  • LFI (Local File Inclusion/Read): atakujący może ukryć w modelu ścieżki typu /home/user/.ssh/id_rsa; podczas ładowania Keras wczyta treść pliku do słownika warstwy.
  • SSRF: Keras używa tf.io.gfile, które obsługuje zdalne systemy plików i protokoły (HTTP/HTTPS). Specjalnie przygotowany URL może spowodować żądania sieciowe (np. do IMDS 169.254.169.254 w chmurze).

Co zmieniła poprawka?

PR #21751 sprawił, że:

  1. Słowniki są osadzane bezpośrednio w archiwum .keras, dzięki czemu ładowanie nie sięga po zewnętrzne ścieżki.
  2. Przy safe_mode=True zabroniono ładowania zewnętrznych plików słownika — dopuszczalne źródło to wyłącznie zawartość archiwum. Zmiana jest częściowo „breaking”. Zmergowano 17 października 2025 r. i wydano jako Keras 3.11.4.

Praktyczne konsekwencje / ryzyko

  • Eksfiltracja sekretów dewelopera/serwera: klucze SSH, tokeny w plikach konfiguracyjnych, .env, ~/.aws/credentials mogą zostać „wciągnięte” do modelu i odczytane przez napastnika po jego ponownym pobraniu lub przez kontrolowany przez niego pipeline.
  • SSRF do usług wewnętrznych: dostęp do IMDS (IAM w chmurze), wewnętrznych API, brokerów metadanych — w konsekwencji przejęcie zasobów chmurowych lub CI/CD.
  • Łańcuch dostaw modeli: ryzyko dotyczy repozytoriów modeli, notebooków, benchmarków, konkursów, gdzie użytkownicy chętnie ładują cudze artefakty.

Rekomendacje operacyjne / co zrobić teraz

1) Natychmiastowa aktualizacja

  • Uaktualnij do Keras ≥ 3.11.4 w środowiskach, które w jakikolwiek sposób ładują cudze .keras. # sprawdź wersję python - <<'PY' import keras, sys print("Keras:", keras.__version__) PY # aktualizacja (przykład dla pip) python -m pip install --upgrade "keras>=3.11.4"

2) Wymuś bezpieczne ładowanie i walidację konfiguracji

  • Przed load_model() otwórz archiwum .keras i przejrzyj konfigurację warstw; odrzuć modele, w których StringLookup/IntegerLookup mają vocabulary jako ścieżkę/URL. import json, zipfile, re, sys from urllib.parse import urlparse def is_path_or_url(v): if not isinstance(v, str): return False if re.match(r'^[a-zA-Z]:[\\/]|^/|^\./|\.\./', v): # Windows/Unix path return True try: u = urlparse(v) return u.scheme in {"http","https","gs","hdfs"} except: return False def keras_archive_has_external_vocab(keras_path): with zipfile.ZipFile(keras_path) as z: with z.open("config.json") as f: cfg = json.load(f) bad = [] def walk(d): if isinstance(d, dict): if d.get("class_name") in {"StringLookup","IntegerLookup"}: vocab = d.get("config",{}).get("vocabulary", None) if is_path_or_url(vocab): bad.append((d["class_name"], vocab)) for v in d.values(): walk(v) elif isinstance(d, list): for v in d: walk(v) walk(cfg) return bad path = sys.argv[1] offenders = keras_archive_has_external_vocab(path) if offenders: print("BLOCK:", offenders); sys.exit(1) else: print("OK") (Skrypt defensywny — nie ładuje modelu, wyłącznie statycznie analizuje archiwum.)
  • Ładuj modele zawsze z safe_mode=True (po aktualizacji ma znaczenie): import keras model = keras.saving.load_model("model.keras", safe_mode=True)

3) Odseparuj IO modelu

  • Uruchamiaj proces ładujący w sandboxie bez dostępu do sekretów (AppArmor/SELinux, no-new-privileges, fs.protected_hardlinks, profile Seccomp).
  • Read-only root, odmontowane /home, brak dostępu do ~/.ssh, brak zmiennych środowiskowych z sekretami.

4) Zablokuj SSRF u źródła

  • Egress deny z jobów ładujących modele (Kubernetes NetworkPolicy, eBPF Cilium, VPC egress firewall).
  • Blokada IMDS:
    • AWS: metadane v2 tylko z hop-limit=1, lub --http-endpoint disabled na EC2; w EKS — AwsNode.kubeProxyEnabled=false + iptables/CNI do odcięcia 169.254.169.254.
    • GCP/Azure analogicznie — polityki blokujące dostęp z podów.
      (Ogranicza efekt SSRF opisany w analizie.)

5) Detekcja w pipeline’ach

  • Skanuj artefakty .keras tymczasowo przed dopuszczeniem do trenowania/inferencji (skrypt powyżej).
  • Alertuj na outbound z jobów ładowania modeli (np. reguły w eBPF/XDP, IDS w podsieci).
  • DLP/Git scanning: reguły wykrywające ciągi typu StringLookup + vocabulary z wartością zaczynającą się od / lub http.

6) Zarządzanie zaufaniem do modeli (MLSBOM)

  • Wprowadzaj Model SBOM (pochodzenie, hash, autor, podpis).
  • Wymagaj podpisu kryptograficznego artefaktów modelu (Cosign/Sigstore) i weryfikuj go w CI przed load_model().

Różnice / porównania z innymi przypadkami

  • Deserializacja nieufnych danych (CWE-502) bywa znana z pickle/joblib. Tu wektorem jest format .keras i specyficzna opcja vocabulary w warstwach *Lookup. Mechanizm safe_mode miał ograniczyć ryzyko, ale nie obejmował ścieżek słowników — stąd podatność.
  • W odróżnieniu od klasycznych RCE przez deserializację, tutaj mamy LFI/SSRF, co i tak może prowadzić do pełnej kompromitacji (np. przejęcie IAM i lateral movement).

Podsumowanie / kluczowe wnioski

  1. Aktualizacja do Keras 3.11.4 jest najważniejsza.
  2. Nie ładuj niezweryfikowanych modeli .keras — traktuj je jak binaria z internetu.
  3. Waliduj konfigurację warstw *Lookup przed deserializacją; blokuj egress i IMDS w jobach ML.
  4. Segmentacja i sandbox procesu ładującego + telemetria wyjść sieciowych minimalizują skutki ewentualnego SSRF.
  5. Wdrażaj łańcuch zaufania do modeli (podpisy, SBOM), jak w klasycznych łańcuchach dostaw.

Źródła / bibliografia

  • SecurityWeek: ogłoszenie luki i informacja o wersji naprawczej 3.11.4. (SecurityWeek)
  • Zscaler (ThreatLabz): analiza techniczna, scenariusze eksploatacji (LFI/SSRF), dotknięte wersje ≤3.11.3. (Zscaler)
  • NVD: karta CVE-2025-12058, opis wektora (LFI/SSRF), CVSS 4.0 5.9 (CNA), powiązane CWE-502. (NVD)
  • GitHub (keras-team), PR #21751: szczegóły implementacji poprawki (osadzanie słownika, restrykcja safe_mode). (GitHub)
  • Dokumentacja Keras: kontekst działania StringLookup. (keras.io)