YAML Metadata Warning:empty or missing yaml metadata in repo card

Check out the documentation for more information.

modelscan Keras .keras RCE blindspot β€” __lambda__ carrier in a keras.ops.vectorized_map (VectorizedMap) operation

Severity: High (arbitrary code execution; scanner reports a fully clean bill) Affected tool: modelscan 0.8.8 β€” KerasLambdaDetectScan (native .keras v3 zip). Also applies to the H5 path (H5LambdaDetectScan). Victim loader: keras.models.load_model(path, safe_mode=False) (keras 3.15.0 / TF 2.21.0) β€” the documented path for loading Lambda-bearing models. Category: ModelScan scanner-bypass on Keras .keras/.h5 (Model File Formats, in-scope).

Summary

modelscan flags a Keras model as dangerous only when a top-level config.layers[] entry has class_name == "Lambda". keras.ops.vectorized_map(fn, x) used inside a functional model serializes as a top-level layer entry whose class_name is "VectorizedMap" (a keras.src.ops.core.Operation), carrying the attacker's Python callable as a {"class_name":"__lambda__", ...} value in its function config field. Because the class name is not "Lambda", modelscan never inspects it and returns total_issues: 0, errors: [], scanned: [evil] β€” a genuine clean result. At load, VectorizedMap.from_config bare-deserializes that field (no module_objects, so no short-circuit) via serialization_lib.deserialize_keras_object β†’ __lambda__ branch β†’ python_utils.func_load β†’ marshal.loads β†’ a real FunctionType, which VectorizedMap.call then invokes (backend.core.vectorized_map(self.function, elements)) during model build/first inference = arbitrary code execution.

Root cause

  • Scanner gap (both formats):
    • modelscan/scanners/keras/scan.py _get_keras_operator_names β€” only layer.get("class_name") == "Lambda" in top-level config.layers.
    • modelscan/scanners/h5/scan.py _get_keras_h5_operator_names β€” same Lambda-only top-level check.
    • modelscan/settings.py β€” unsafe_keras_operators = {"Lambda": "MEDIUM"} only. "VectorizedMap" is not listed and sub-fields are never inspected.
  • Exec sink (victim side, keras 3.15.0):
    • keras/src/ops/core.py:1163-1167 β€” VectorizedMap.from_config: config["function"] = serialization_lib.deserialize_keras_object(config["function"]) (bare, no module_objects β†’ does not short-circuit __lambda__).
    • keras/src/saving/serialization_lib.py:686 β€” class_name == "__lambda__" β†’ python_utils.func_load(inner_config["value"]).
    • keras/src/utils/python_utils.py func_load β†’ marshal.loads β†’ types.FunctionType.
    • keras/src/ops/core.py:1139 β€” VectorizedMap.call β†’ backend.core.vectorized_map(self.function, elements) calls the reconstructed function.

Distinct from previously-filed Keras/modelscan carriers

  • Not the TextVectorization standardize/split carrier (different layer, different field).
  • Not the "module"-key legacyβ†’modern deserializer fork on .h5 activation (this needs no injected "module" key; VectorizedMap.from_config bare-deserializes natively, and the class is not a Dense/activation layer).
  • Not the nested-Functional-submodel real-Lambda carrier (there is no Lambda layer anywhere β€” class_name is "VectorizedMap", a distinct built-in operation).
  • Not the training_config/compile loss/metric blindspot (this is a forward-pass model op firing at build/inference, not a compile-time callable).
  • Among all keras.ops operations, VectorizedMap is the one that both stores its function in get_config and bare-deserializes + invokes it (Map/Scan/Cond/Switch/… take functions as call-time args and do not round-trip a stored function through model config).

Reproduce

Env: pip install tensorflow keras 'modelscan==0.8.8', KERAS_BACKEND=tensorflow (Python 3.10–3.12).

python build_poc.py          # writes evil_vectorizedmap.keras + control_lambda.keras
modelscan -p evil_vectorizedmap.keras     # -> No issues found! (total_issues: 0, errors: [])
modelscan -p control_lambda.keras         # -> 1 MEDIUM 'Lambda' (control: detection works)
python demo_load.py          # load_model(safe_mode=False) + predict -> /tmp/VMAP_PWN.txt written

Verified output (modelscan 0.8.8, keras 3.15.0, TF 2.21.0)

  • evil_vectorizedmap.keras β€” modelscan JSON: total_issues: 0, total_issues_by_severity all 0, errors: [], scanned_files: ["evil_vectorizedmap.keras"] (see modelscan_evil_result.json). Top-level layer class_names are ["InputLayer", "VectorizedMap"] β€” no "Lambda".
  • Execution: keras.models.load_model("evil_vectorizedmap.keras", safe_mode=False) fires the payload during load (functional trace) and predict() completes normally (output shape (2,3)); marker /tmp/VMAP_PWN.txt = VMAP_RCE_1000 (real os.system / id -u).
  • Control 1 (detection works): control_lambda.keras (identical payload wrapped in a real Lambda) β†’ modelscan total_issues: 1, MEDIUM, "Use of unsafe operator 'Lambda'" (see modelscan_control_result.json).
  • Control 2 (exec gate): default load_model(...) (safe_mode=True) raises ValueError: Requested the deserialization of a Python lambda ... and the marker is not written β€” payload only fires under the documented safe_mode=False.

Impact

ACE against any pipeline that scans an untrusted .keras (or .h5) model with modelscan β€” receiving a clean, 0-issue, file-marked-scanned result β€” and then loads it via the standard load_model(safe_mode=False). No victim-side custom class and no Lambda layer present, so it defeats modelscan's "look for Lambda layers" detection model with a benign-looking built-in Keras operation.

Files

  • build_poc.py β€” regenerates both models (payload is benign: writes /tmp/VMAP_PWN.txt with id -u).
  • evil_vectorizedmap.keras β€” malicious model (class_name VectorizedMap, no Lambda).
  • control_lambda.keras β€” negative control (same payload as a real Lambda; modelscan flags it).
  • demo_load.py β€” loads the evil model and triggers the payload.
  • modelscan_evil_result.json / modelscan_control_result.json β€” captured scanner output.

Suggested fix

In both Keras scanners, recurse all layer/operation entries and normalize sub-fields, flagging any class_name == "__lambda__" anywhere in the config (and treat function-carrying operations like VectorizedMap as unsafe), rather than matching only a top-level class_name == "Lambda".

Downloads last month
55
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support