Keras

You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

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 tiny Dense layer named "a"*28, compiled with MultiOptimizer(OptimizerMap(default_optimizer=SGD(), optimizer_map={".*(a+)+$": SGD()})).
  • build_multiopt_model.py β€” script that built multiopt_bomb.keras (for reproducibility from a clean checkout).
  • trigger_multiopt_redos.py β€” loads multiopt_bomb.keras with keras.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
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support