Keras
EnigmaConsultant's picture
Upload README.md with huggingface_hub
0dae0bf verified
|
Raw
History Blame Contribute Delete
4.18 kB

modelscan .keras (v3 zip) Lambda-detection bypass via Lambda nested in a Functional submodel

Severity: Medium (matches modelscan's own MEDIUM rating for a detected Lambda; RCE-on-load gated by safe_mode=False) Affected tool: modelscan 0.8.8 β€” scanners/keras/scan.py KerasLambdaDetectScan. Victim loader: keras.saving.load_model(safe_mode=False) (keras 3.14.1). Category: ModelScan scanner-bypass on .keras (Keras v3 zip).

Summary

Companion to the .h5 nested-submodel finding, against the distinct .keras-zip scanner code path + file format. KerasLambdaDetectScan reads config.json from the .keras zip and extracts operators with a single non-recursive list comprehension over the top-level config["layers"], flagging only top-level class_name == "Lambda". Wrapping the malicious Lambda in a one-layer Functional submodel moves it to config.layers[i].config.layers[j]; the top-level list then contains class_name "Functional" (not "Lambda") β†’ scanner returns [] β†’ "No issues found! πŸŽ‰" (API and CLI). At load, functional_from_config recurses into the submodel and Lambda.from_config β†’ python_utils.func_load(marshal.loads(...)) executes the attacker code.

Root cause

modelscan/scanners/keras/scan.py:119-130 (_get_keras_operator_names): flat comprehension for layer in model_config_data.get("config", {}).get("layers", {}) flagging only top-level class_name == "Lambda"; never descends into a nested sub-Model's config.layers.

Exec sink: keras/src/models/functional.py::functional_from_config β†’ serialization_lib.deserialize_keras_object (recursive) β†’ keras/src/layers/core/lambda_layer.py:182-198 Lambda.from_config β†’ python_utils.func_load.

Reproduce

python poc/poc_full.py (env: keras 3.14.1, modelscan 0.8.8; payload built with keras' own func_dump = real marshalled bytecode, save-equivalent). Output:

  • (1) baseline top-level Lambda at /config/layers[1] β†’ modelscan operators ['Lambda'] (flagged).
  • (2) nested Lambda at /config/layers[2]/config/layers[1] β†’ top-level class_names ['InputLayer','Dense','Functional','Dense'], modelscan operators [] (bypass).
  • (3) load_model(safe_mode=False) β†’ marker written (RCE).
  • (4) load_model(safe_mode=True) β†’ blocked by keras ValueError (so the warning modelscan failed to emit was the user's only safeguard).
  • CLI cross-check: modelscan -p nested.keras β†’ KerasLambdaDetectScan β†’ "No issues found! πŸŽ‰".

Empirically distinguished from the TimeDistributed dup: TimeDistributed(Lambda) puts the Lambda at config/layers[1]/config/layer (single layer key); this finding's path is config/layers[i]/config/layers[j] (nested submodel layers list).

Impact

Defeats modelscan's gate completely for the .keras v3 format: a code-executing model is certified clean, and a consumer who loads it with safe_mode=False (required for every genuine Lambda model, routine in HF/third-party code) is compromised. Severity matches modelscan's MEDIUM for a detected Lambda; the bypass makes that detection 0%.

Dup-check

Not public for this vector. CVE-2025-1550 + huntr blog = top-level Lambda (modelscan flags those). CVE-2025-9905 = HDF5 safe_mode-ignore (different format/mechanism). arXiv:2509.06703 KV.1/KV.2 = top-level self-disabling Lambdas. The "29 ways" article lists only TimeDistributed for Keras Lambda nesting (re-fetched; no mention of nested Functional/Sequential submodels). Distinct from our R5 __lambda__-in-TextVectorization and the companion R6 H5 nested-submodel (different scanner class + format).

Honest caveat (the reason this is Medium, not higher): the root cause β€” non-recursive top-level-only layer scan β€” is identical to the already-known TimeDistributed bypass and to the companion .h5 finding. A single fix closes all three, so a maintainer may legitimately treat this as a duplicate-by-root-cause / bundle it. It clears the bar as a genuinely distinct, undocumented config path against a distinct scanner method + format with a fully working RCE β€” but file expecting possible consolidation.