Security Operations
Stored XSS in Markdown CMS Previews: A Review Guide
Protect Markdown previews and rendered articles with safe HTML handling, URL validation and separate editor tests that catch stored XSS before publication.
In this article
Stored XSS in a Markdown CMS happens when saved content reaches a browser as executable markup or script. A trusted author account does not make every imported article safe: content can arrive from uploads, integrations, compromised accounts or copied examples. Review both the editor preview and the published page.
The safe approach depends on how content is rendered. Escape ordinary text, sanitize permitted HTML with a maintained sanitizer, validate link destinations and avoid inserting untrusted strings into executable contexts. A single blacklist of suspicious words is not a reliable defense.
Identify every rendering surface
Follow one article from import JSON through storage, admin preview, public rendering and any export or notification. List where Markdown becomes HTML and where raw HTML is allowed. The preview may use a different library or configuration from the public page.
Include titles, excerpts, image descriptions and SEO fields. A team can protect the main body while accidentally inserting a title into unsafe markup elsewhere. Treat every field according to its output context rather than assuming a text field stays text forever.
Use JSON Viewer to inspect an invented import fixture. This helps locate content fields but does not prove they are safe to render. A JSON parser only establishes syntax and structure.
Separate Markdown parsing from HTML safety
A Markdown parser's job is to interpret Markdown. It is not necessarily a security boundary. Decide whether raw HTML is needed at all. If not, disable or escape it using the parser's supported settings.
When limited HTML is required, apply an allowlist sanitizer after rendering. Permit only the elements and attributes the publishing feature needs. Keep the library current and review changes in its configuration. Sanitizing first and then performing unsafe string substitutions can reintroduce dangerous markup.
OWASP's XSS prevention guidance distinguishes output encoding from HTML sanitization. The practical distinction is simple: display text as text, and apply a dedicated policy when you intentionally allow markup.
Test links and media separately
Reject unsafe URL schemes where links or image sources are created. A normal-looking label does not make its destination appropriate. Decide which schemes are supported by the product, such as HTTPS links or explicitly permitted email links.
Resolve relative URLs according to a documented base. Review redirects and external-image behavior separately. A sanitized image can still disclose a viewer's request metadata to its host; XSS prevention is not the same as privacy protection.
Do not use URL Decoder as a security validator. It can make an encoded fixture easier to read, but a safe renderer must validate the value it actually uses. Inconsistent decoding across layers can create gaps in a policy that appears correct in one view.
Build harmless regression fixtures
Use a local or staging environment you control. Start with visible benign strings containing angle brackets, quotes, ampersands, unexpected attributes and disallowed URL schemes. The expected result is escaped text or removed disallowed markup, never execution.
{"title":"Preview <example>",
"content":"A normal paragraph with a [link](https://example.com)."}Then add security-specific fixtures through your test framework using the sanitizer's documented test cases. Do not paste exploit material into a live production CMS merely to see what happens. Keep tests isolated and assert on rendered output.
Cover the preview's privilege boundary
An admin preview runs near privileged workflows. A rendering defect there can be more consequential than the same defect on a public article. Keep preview documents isolated where practical and avoid granting them unnecessary access to the parent page.
If using a sandboxed frame, understand its actual permissions. Adding broad script and origin permissions can undermine the isolation you expected. A frame alone is not proof that malicious content cannot affect the surrounding workflow.
Also test editing existing records after a sanitizer upgrade. Content stored before a fix may still be rendered by a vulnerable path. You may need a controlled re-render or review process rather than assuming all old content becomes safe automatically.
Use headers as additional protection
A content security policy can reduce some script execution paths, but it does not replace safe rendering. Roll it out with a dependency inventory and deliberate testing. Third-party scripts and editor plugins can complicate a strict policy.
For public pages, inspect the resulting policy with HTTP Header Checker. For authenticated previews, use browser developer tools because a public lookup cannot reproduce your admin session. Review the actual response rather than a configuration file alone.
Release criteria
Require safe rendering in import preview, editor preview and public output. Confirm allowed formatting still works, forbidden markup is removed and link validation matches the documented scheme policy. Keep the fixtures in regression tests so a parser replacement cannot silently undo the controls.
Make content safe at the point of use
Stored content should be validated structurally on import and rendered safely in every browser surface. Keep parsing, sanitization and URL handling distinct. That protects the authoring workflow while preserving the formatting readers need.