PoC β ModelScan does not scan .hdf5 files and reports them as clean
Security research artefact for a huntr Model File Vulnerability report. Not a usable model. Do not load these files outside a disposable sandbox.
What is in this repo
Two byte-identical copies of one Keras model:
| file | sha256 |
|---|---|
sentiment_model.h5 |
4b936709f91d4c46a9dbf6cd73bcdbeb10e46aed9f24add1acf4da7d401efb80 |
sentiment_model.hdf5 |
4b936709f91d4c46a9dbf6cd73bcdbeb10e46aed9f24add1acf4da7d401efb80 |
The only difference is the filename extension.
The model is an ordinary 4-layer Sequential (Dense β Lambda β Dense).
The Lambda layer carries a marshalled anonymous lambda. The payload is
inert: it creates /tmp/huntr_exec/PWNED and returns its input unchanged. It
is a stand-in for arbitrary code β nothing here is destructive.
The issue
ModelScan classifies a file purely by exact filename suffix
(modelscan/middlewares/format_via_extension.py):
extension = model.get_source().suffix
formats = [
format
for format, extensions in self._settings["formats"].items()
if extension in extensions
]
settings.py maps KERAS_H5: [".h5"]. It does not map .hdf5.
When nothing matches, the file gets no format context, every scanner returns
early on its if FORMAT not in model.get_context("formats") guard, and the file
is never inspected β yet the run still prints No issues found! π. The skip is
visible only behind the non-default --show-skipped flag.
Meanwhile keras.saving.load_model accepts both extensions identically
(keras/src/saving/saving_api.py:83,196):
if str(filepath).endswith((".h5", ".hdf5")):
So the two files are the same model to the loader, and different files to the scanner.
Reproduction
pip install "modelscan[tensorflow]"
modelscan -p sentiment_model.h5
# Total Issues: 1
# - Severity: MEDIUM
# - Description: Use of unsafe operator 'Lambda' from module 'Keras'
modelscan -p sentiment_model.hdf5 --show-skipped
# No issues found! π
# Skipped files list:
# The following file sentiment_model.hdf5 was skipped during a ModelScan scan:
# Model Scan did not scan file
Confirming both files are the same model to Keras (run in a sandbox):
import keras
keras.saving.load_model("sentiment_model.hdf5", safe_mode=False, compile=False)
# -> loads; /tmp/huntr_exec/PWNED now exists
Measured results
modelscan (latest, tensorflow extras) / keras 3.15.0:
| file | ModelScan verdict | load_model(safe_mode=True) |
load_model(safe_mode=False) |
|---|---|---|---|
sentiment_model.h5 |
Total Issues: 1 (MEDIUM) | blocked | executes |
sentiment_model.hdf5 |
No issues found β not scanned | blocked | executes |
Scope note
Keras 3.15.0 correctly enforces safe_mode=True for both files, so this is not
a Keras vulnerability and no Keras RCE is claimed. The defect is in ModelScan:
it is the control used to decide whether a model file is safe before it is
loaded, and it returns "clean" for a file it never opened. Anyone loading with
safe_mode=False β required for any legitimate Lambda-bearing model, and the
usual workaround when a load fails β gets code execution on a file the gate
passed.
.hdf5 is a standard, documented HDF5 extension, and ModelScan's README states
it supports "H5".
Suggested fix
Classify by content, not by suffix β HDF5 files begin with the magic bytes
\x89HDF\r\n\x1a\n. And when a file cannot be classified, surface it as an
explicit unscanned result in the default summary instead of folding it into
"No issues found".