Instructions to use EnigmaConsultant/keras-multioptimizer-redos-poc with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Keras
How to use EnigmaConsultant/keras-multioptimizer-redos-poc with Keras:
# Available backend options are: "jax", "torch", "tensorflow". import os os.environ["KERAS_BACKEND"] = "jax" import keras model = keras.saving.load_model("hf://EnigmaConsultant/keras-multioptimizer-redos-poc") - Notebooks
- Google Colab
- Kaggle
Keras MultiOptimizer / OptimizerMap ReDoS via load_model() (CWE-1333)
Target: github.com/keras-team/keras, commit 2aeecf06e0dc6886d962d14f8056f7ca8b15792b
(2026-07-01), .keras native format loader (keras.saving.load_model).
Access is gated β this repo contains a working denial-of-service proof of
concept (a .keras model file that hangs the loading process). It is shared
for the huntr triage/maintainer team only.
Summary
keras.optimizers.MultiOptimizer delegates each trainable variable to a
sub-optimizer chosen by keras.optimizers.OptimizerMap.__getitem__
(keras/src/optimizers/multi_optimizer.py:92):
matching_keys = [
pattern
for pattern in self._optimizer_map.keys()
if re.fullmatch(pattern, key)
]
self._optimizer_map.keys() are ordinary Python strings taken straight from
the model's compile_config (OptimizerMap.get_config() /
from_config() round-trips them verbatim as dict keys), and key is
variable.path, which is built from layer names that are equally
attacker-controlled (config.json's layer "name" field). An attacker who
authors a .keras file therefore controls both sides of a
re.fullmatch() call: the regex pattern and the string it is matched
against.
Crucially, this isn't a lazily-triggered, training-time-only bug. Loading a
compiled model calls compile_from_config()
(keras/src/trainers/trainer.py:1020):
if hasattr(self, "optimizer") and self.built:
# Create optimizer variables.
self.optimizer.build(self.trainable_variables)
MultiOptimizer.build() (keras/src/optimizers/multi_optimizer.py:255) then
calls self._optimizer_map(var) for every trainable variable, i.e. it
performs the re.fullmatch() evaluation, eagerly, during
load_model() itself β no training step, no model.fit(), no extra
victim action required. compile=True is the default of
keras.saving.load_model.
Picking optimizer_map = {".*(a+)+$": some_optimizer} and a layer named with
a run of "a" characters (e.g. 28 of them) is a textbook catastrophic
backtracking construction: the string never fully matches (it ends in
/kernel, not a), so the regex engine explores an exponential number of
groupings before concluding failure. A ~28-byte layer name is enough to pin a
CPU core for ~25 seconds; a few dozen more bytes (still a trivial addition to
config.json, no larger file needed) push this past any reasonable timeout β
an effectively permanent hang that the caller cannot recover from because it
happens synchronously inside the C-implemented re engine (no Python-level
loop to interrupt short of killing the process).
This is the same bug class (CWE-1333, config-controlled regex ReDoS on the
model-loading path) that Keras has already accepted as security-relevant and
is in the process of fixing elsewhere β keras/src/dtype_policies/dtype_policy_map.py
(open PR #23102) and keras/src/optimizers/muon.py's exclude_layers
(open PR #23162) β but neither of those PRs touches
keras/src/optimizers/multi_optimizer.py, and no report against
OptimizerMap/MultiOptimizer exists (verified via GitHub code/issue search
on 2026-07-06). It is also more directly reachable than the Muon variant:
the Muon PR's own description says the hang fires "on the first gradient step
after the model is loaded with compile=True and trained"; this one fires
during load_model() itself, before any training step, because
compile_from_config builds the optimizer eagerly.
Files
multiopt_bomb.kerasβ the malicious model. A tinyDenselayer named"a"*28, compiled withMultiOptimizer(OptimizerMap(default_optimizer=SGD(), optimizer_map={".*(a+)+$": SGD()})).build_multiopt_model.pyβ script that builtmultiopt_bomb.keras(for reproducibility from a clean checkout).trigger_multiopt_redos.pyβ loadsmultiopt_bomb.keraswithkeras.saving.load_model(path, compile=True)under a SIGALRM budget and reports whether the call completed or hung.
Reproduction (verified against keras @ 2aeecf06e0dc6886d962d14f8056f7ca8b15792b)
$ python3 trigger_multiopt_redos.py multiopt_bomb.keras 25
[+] Loading multiopt_bomb.keras (compile=True) -- this is exactly what a victim
would do with a model file received from someone else ...
[+] load_model() returned OK in 24.67s (the ReDoS regex is only *evaluated*
lazily, on the first gradient application -- not at load time)
(That "returned OK in 24.67s" message is emitted by the harness only in the
success branch; in this run load_model() itself took 24.67 seconds β for a
28-byte layer name β entirely inside re.fullmatch(), confirming the hang
happens synchronously during load_model(), not lazily at training time as
the harness comment speculated before instrumentation. The traceback that
follows is model.fit() raising NotImplementedError for the numpy backend,
which is irrelevant β the DoS had already completed by that point.)
Isolated regex timing (same pattern/string shape, standalone re module,
demonstrates the exponential blow-up independent of Keras):
n= 10 len= 19 match=None time=0.0002s
n= 14 len= 23 match=None time=0.0008s
n= 18 len= 27 match=None time=0.0117s
n= 20 len= 29 match=None time=0.0466s
n= 22 len= 31 match=None time=0.1896s
n= 24 len= 33 match=None time=0.7559s
n= 26 len= 35 match=None time=3.1496s
(n = count of "a" characters in the probed string; time roughly
quadruples every +2 characters, the signature of catastrophic
backtracking.)
Impact
Denial of service. Any application that loads user-, customer-, or
internet-supplied .keras files (a common pattern: HF Hub model
mirrors/zoos, MLOps pipelines accepting uploaded models, CI that loads
externally produced artifacts) can be hung indefinitely β pinning a CPU core
and blocking that worker/process β by a .keras file that is otherwise a
completely unremarkable, tiny, valid model. compile=True is the default,
so no non-default flag is required.
Suggested fix
Mirror the fix already applied/proposed for the structurally identical
DTypePolicyMap (#23102) and Muon.exclude_layers (#23162) cases: validate
OptimizerMap keys at construction / deserialization time against an
allowlist grammar that admits ordinary path-keyword patterns (a run of
path characters, optionally followed by a single trailing .*) but rejects
nested quantifiers, and reject any key when the model is loaded, before
build() ever runs re.fullmatch on it.
- Downloads last month
- 16