VAITP Dataset

← Back to the dataset

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.

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 ::