fix: Escape markdown preview HTML #53

Merged
rockachopa merged 1 commits from timmy/52-escape-markdown-preview-html into main 2026-08-05 23:49:10 +00:00
4 changed files with 59 additions and 8 deletions

View File

@ -159,6 +159,7 @@ textarea { resize: vertical; min-height: 120px; }
<div class="footer">Creative AI-imbued UI • stackchain-dashboard</div>
<script src="/static/markdown.js"></script>
<script>
(function(){
const qs = (s, el=document) => el.querySelector(s);
@ -327,14 +328,7 @@ textarea { resize: vertical; min-height: 120px; }
qs('#md-input').addEventListener('input', renderMD);
function renderMD() {
const raw = qs('#md-input').value || '';
const html = raw.replace(/^#{1,6}\s.*$/gm, (m)=>{ const lvl=m.match(/^(#{1,6})/)[1].length; return '<h'+lvl+'>'+m.replace(/^#{1,6}\s/,'')+'</h'+lvl+'>'; })
.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>')
.replace(/\*(.+?)\*/g,'<em>$1</em>')
.replace(/`([^`]+)`/g,'<code>$1</code>')
.replace(/^\-\s?(.+)$/gm,'<li>$1</li>')
.replace(/(<li>.*<\/li>)/s,'<ul>$1</ul>')
.replace(/\n/g,'<br>');
qs('#md-preview').innerHTML = '<pre>' + escapeHtml(raw) + '</pre><div style="margin-top:8px;">' + html + '</div>';
qs('#md-preview').innerHTML = '<pre>' + escapeHtml(raw) + '</pre><div style="margin-top:8px;">' + renderMarkdown(raw) + '</div>';
}
/* Commands */

29
frontend/markdown.js Normal file
View File

@ -0,0 +1,29 @@
(function (root, factory) {
const renderMarkdown = factory();
if (typeof module === 'object' && module.exports) module.exports = renderMarkdown;
root.renderMarkdown = renderMarkdown;
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
function escapeHtml(value) {
return String(value || '').replace(/[&<>"']/g, (character) => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
})[character]);
}
return function renderMarkdown(raw) {
return escapeHtml(raw)
.replace(/^#{1,6}\s.*$/gm, (line) => {
const level = line.match(/^(#{1,6})/)[1].length;
return '<h' + level + '>' + line.replace(/^#{1,6}\s/, '') + '</h' + level + '>';
})
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/^-\s?(.+)$/gm, '<li>$1</li>')
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
.replace(/\n/g, '<br>');
};
});

View File

@ -1,5 +1,6 @@
import asyncio
import math
from pathlib import Path
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
@ -13,6 +14,7 @@ from src.views import router as frontend_router
app = FastAPI(title="Stackchain Dashboard")
CONTEXT_TIMEOUT_SECONDS = 5.0
FRONTEND_DIR = Path(__file__).resolve().parent.parent / "frontend"
app.add_middleware(
CORSMiddleware,
@ -22,6 +24,7 @@ app.add_middleware(
allow_headers=["*"],
)
app.mount("/static", StaticFiles(directory=FRONTEND_DIR), name="static")
app.include_router(frontend_router)

View File

@ -0,0 +1,25 @@
import json
import subprocess
from pathlib import Path
RENDERER = Path(__file__).parent.parent / "frontend" / "markdown.js"
def test_markdown_renderer_escapes_raw_html_before_rendering_heading():
payload = "# <img src=x onerror=alert(document.domain)>"
script = (
f"const render = require({json.dumps(str(RENDERER))});"
f"process.stdout.write(render({json.dumps(payload)}));"
)
result = subprocess.run(
["node", "-e", script],
check=True,
capture_output=True,
text=True,
)
assert result.stdout == (
"<h1>&lt;img src=x onerror=alert(document.domain)&gt;</h1>"
)