Markdown Stored XSS: When HTML in Markdown Becomes Stored XSS
Markdown feels safe because it is “not HTML.” Most pipelines parse Markdown to an AST, then serialize to HTML. If the serializer allows raw HTML blocks, or a plugin is loose with attributes, you get stored XSS in READMEs, ticket comments, product descriptions, and knowledge bases.
Payloads that still land
Start boring:
- Raw
<script>alert(1)</script>in a fenced-free HTML block [click](javascript:alert(1))depending on renderer allowlists<img src=x onerror=…>inside allowed HTML- SVG
<svg onload=…>when sanitizer allowlistssvg <a href="data:text/html;base64,…">for nested browsing contexts
CommonMark parsers differ. GitHub Flavored Markdown is not the same as a random marked + sanitize: false stack. Test the target’s actual output, not a generic cheat sheet.
Where the sink hides
Comment previews vs saved views may use different sanitizers — I’ve caught XSS only after save. Emails that re-render Markdown can fire in a different HTML client (harder proof). Admin “CMS markdown” fields are higher impact than public profiles because privileged cookies sit on those origins.
Look at dangerouslySetInnerHTML in React bundles and server-side Markdown libraries in stack traces or package.json leaks.
Sanitizer bypass mindset
If <script> dies but <math> or obscure tags live, dig. If on* attributes are stripped yet href is not scheme-validated, use javascript: or vbscript: where legacy browsers in scope still matter. Mutation XSS (mXSS) appears when the sanitizer cleans a string the browser reinterprets after insertion — rarer, but real with certain parser pairs.
CSP can save a weak sanitizer. Weak CSP (unsafe-inline, missing script-src) makes Markdown bugs critical again. Check both layers.
Proof without being rude
Use unique canaries (alert with your hunter handle, or a harmless DOM change). Prefer self-account stored XSS first. For cross-user impact, post as low-priv and view as admin/moderator if the program allows. Do not phish real customers.
Report essentials
Show the Markdown source, the stored location, the rendered HTML sink, and a screenshot of script execution in the victim session context. Note cookie flags (HttpOnly still allows session theft via API calls from XSS). Map to CWE-79 and PortSwigger stored XSS guidance.
Fixes that stick
Disable raw HTML in Markdown unless you run a strict allowlist sanitizer after render (DOMPurify or equivalent, configured tightly). Scheme-check links. Avoid unsafe-inline CSP. Treat every Markdown field as untrusted input — including admin-only fields (admins get XSS’d too).
If the product markets “safe Markdown,” your job is to show the HTML it actually emits. One onerror in a release note is enough.