In a Markdown file, if you encounter “<script>”, the editor might simply display it as plain text, while the rendering engine might retain the original HTML. The file extension itself doesn’t guarantee security; the actual boundaries lie in the source of the content, the parsing configuration, and the final output method.
This document distinguishes between two scenarios: Git content maintained by trusted authors, and content submitted by public users. The former can accept higher editing permissions, while the latter must be treated as untrusted input. We should not assume that, because there is only one author currently, we can connect the future comment interface to the same rendering pipeline.
The Same Content Has Different Semantics at Different Outputs
After cleaning, the main text of the article is used for the webpage. The summary may be used for attributes, the title may be used in JSON, and links may be used in RSS. A single HTML cleaning process does not guarantee safety in all contexts. HTML text, attributes, script data, and URLs all have their own encoding rules, and should be handled by the corresponding framework and structured API. Do not treat “cleaned strings” as a universal, safe value that can be directly concatenated in any location.
Assuming the submitted title contains quotes, the standard template output should correctly escape them. However, manually entering <meta content="${title}"> might disrupt the attribute. Code examples within the body should be treated as text, not executed. The full RSS feed also requires valid XML and appropriate HTML semantics. Each output should be derived from clearly defined data fields, not arbitrarily copied from pre-built HTML pages.
It’s also important to distinguish between editing permissions and execution permissions for trusted authors. Authors should be able to modify the main content without automatically receiving build environment tokens. If a multi-author repository allows importing any module via MDX, then code review and isolation of the execution environment are essential. The trustworthiness of a contributor’s identity does not guarantee that their account will never be compromised, nor does it mean that every copied code snippet has been checked.
Output the content as plain text, and the framework typically escapes special characters. When injecting the rendered HTML into the page, it requires that the input has been properly sanitized. A Markdown parser that allows HTML will not automatically know which attributes, protocols, or tags can execute scripts.
// Pipeline sketch; verify plugin configuration against the project version.
const html = markdownToHtml(untrustedMarkdown);
const safeHtml = sanitizeWithAllowlist(html);
The sanitizer should use a mature and well-established approach, and should adhere to a whitelist. Simply using regular expressions to remove <script> is not sufficient. Event attributes, SVG, dangerous URL protocols, and complex nesting can bypass this type of sanitization. Sanitization should occur in the final HTML stage, and after sanitization, no untrusted plugins that add arbitrary HTML should be used.
Review Sanitization Order Together with Plugin Capabilities
After Markdown parsing, highlighting plugins might insert <span> tags, linking plugins might add attributes, and charting plugins might generate SVG files. If cleaning is done too early, subsequent plugins can re-insert uncontrolled HTML. If cleaning is done too late and the rules are too restrictive, valid highlighting or charting will be disrupted. It’s important to first identify the trusted conversion stages and then use a whitelist in the final output. The cleaning tool itself also needs to be maintained and tested, and should not be installed and never updated.
The whitelist should be organized by functionality whenever possible. Technical articles should include paragraphs, headings, lists, code snippets, citations, and regular images, but do not necessarily require iframes, forms, or any specific styling. When embedding videos, use controlled component mapping instead of exposing all iframe attributes. SVG files can contain complex features; if they originate from untrusted sources, they should be processed using a mature sanitization scheme or converted into controlled static images. It is not safe to assume that a file is secure simply by removing a script tag.
Also, the ID and anchor points should be checked. When generating IDs for titles, avoid conflicts with existing important elements on the page and maintain link safety. Duplicate titles require stable deduplication, and the directory should not generate dangerous properties due to user input containing quotes. The names of DOM attributes and JavaScript object access can also be affected by special values. Plugin output should use a structured tree instead of string concatenation. Security reviews should focus on the complete conversion chain, rather than just checking whether there are script tags in the main content.
Link protocols require independent rules
Allowing HTTPS and internal relative links is generally more suitable for blog needs than allowing any protocol. Data URLs and protocols for opening external applications (e.g., javascript:) should be restricted based on the specific scenario. When using external links in new windows, use the appropriate rel attribute, but do not use it as a substitute for proper security measures.
The images also pose privacy and resource risks. External images send access requests to remote hosts, potentially becoming tracking pixels. Large images increase loading costs. Images fetched through proxies require protection against SSRF (Server-Side Request Forgery), preventing the server from requesting arbitrary internal addresses provided by users. Each “convenient, automated” capability increases a trust boundary.
Directly loading external images through a browser exposes the request to a third party. While a server-side proxy can reduce this exposure, it also introduces the risk of requests originating from any source. If internal addresses, cloud metadata addresses, or redirects to these addresses are allowed, the proxy could potentially access resources it shouldn’t. Allowing only trusted sources, restricting protocols, and re-verifying redirect destinations are common starting points, but relying on simple string checks instead of a comprehensive strategy is not a secure solution.
The proxy also needs to restrict download sizes, response types, timeouts, and conversion dimensions. A legitimate public image may incur significant or unexpected decoding costs, and cannot be determined solely based on the file extension. Limiting the number of cache keys prevents expensive conversions caused by arbitrary combinations of dimensions. Content source licenses also exist independently; technically, a proxy does not imply ownership of the right to rehost.
For personal static sites, if images are only maintained by the author, it’s possible to manage controlled local assets during build time, avoiding the need to expose arbitrary URLs through a proxy. For public submission platforms, a dedicated upload pipeline is required, along with validation and storage in a suitable format, and a stable address. Removing unnecessary features is often safer and simpler than maintaining a general-purpose image proxy.
MDX offers greater permissions than Markdown
MDX can import components and JavaScript expressions, making it suitable for trusted content repositories but not for directly handling public submissions. Even if the final site is static, malicious code executed during the build process could potentially access the CI environment and credentials.
Therefore, submissions should enter a controlled data pipeline, rather than being run as MDX files. Author permissions, dependency installation permissions, and CI permissions should all be considered together. Executing code within plugins during the build process, as well as reviewing the source and version of plugins, are all part of content security.
When an author needs to provide prompts, code labels, or image descriptions, this can be expressed using limited instructions or structured fields, with a trusted component responsible for outputting the information. The editor only selects the component type and text, without importing any modules or passing any event handling functions. This maintains the author’s writing capabilities while still allowing the project code to retain control over execution permissions. Public submissions and administrator articles can use different sets of capabilities, without requiring full submission permissions to meet the needs of a high-level author.
For example, an admonition component can support three variants: info, warning, and note. Its title and body use plain text or controlled Markdown, while the component determines styling instead of accepting author-supplied styles. A link component accepts validated URLs and text; an embed accepts a vendor video identifier and generates a fixed source. Clear parameters make tests more complete and rejection reasons easier to explain to editors.
When rejecting content that is not supported, provide a fixable diagnostic, rather than silently deleting the entire paragraph. If the author sees a preview with missing content but doesn’t know why, they might try more complex HTML workarounds. Field errors can be displayed in the private editing process, but the original dangerous input is not exposed on public pages. Pre-publication rendering difference checks can help identify missing content, and both security and usability should be considered.
For repositories containing already trusted articles, the focus of review should shift to the original HTML, the addition of new components and external resources, rather than simply blocking all advanced features. Once the level of trust and source are clearly defined, the system can provide appropriate capabilities without assuming that “trusted authors” equate to “trusting any byte.” Copied examples, third-party plugins, and compromised accounts still require independent protection.
This design also offers maintenance benefits. Upgrading a single controlled component can change the accessibility and styling of all the alert boxes, while inconsistent hand-written HTML is difficult to migrate. Therefore, the security rules not only restrict writing but also make the article structure more stable; provided that the rules serve real needs and do not force authors to convert meaningful information into images solely for simplification.
Negative Tests Should Check Behavior, Not Appearance
Prepare sample files containing event attributes, dangerous protocols, malformed tags, and raw HTML, then render them in an isolated test environment. Inspect the final DOM and whether the browser executes anything; do not rely only on whether the page text looks correct. Also verify that legitimate code examples are not removed, or readers will receive incomplete technical content.
Code blocks containing legitimate code should retain their text formatting, and should not be converted into executable HTML due to highlighting tools. Plugins that generate title anchors and table of contents should also check for ID and link handling to avoid generating unsafe output due to duplicate or unusual characters.
Finally, CSP (Content Security Policy) is added as a defense-in-depth measure, and the reliance on updates is maintained. Articles already in Git still require manual review, as issues such as incorrect links, misleading information, and leaked configurations are not detectable by the cleaner. Security rendering focuses on execution permissions, while trusted writing focuses on content quality. Neither can be replaced by the other.
A more easily overlooked vulnerability is the embedding of JSON within HTML. While the title itself is safe as plain text, this doesn’t mean that directly injecting JSON.stringify(record) into a <script> element is also safe: The HTML parser first determines where an element ends, and then JavaScript or JSON parsers read the content within. Even if the <script> element is marked as JSON, it’s still necessary to handle the end-of-sequence within the HTML context, rather than simply proving that the object can be serialized.
Testing involves using titles containing synthetic </script> text, verifying that the final DOM still contains only the expected data elements, and then having the consumer parse and compare the original title. This ensures that no unintended nodes are created and that the literal text displayed by the author is preserved. Using a framework-supported secure serialization mechanism, a set of character replacements is not invented within the article; if the same JSON is sent independently as an application/json response, it carries different boundaries.
Once the data is read, it enters a second context. After the search component parses the JSON, if the title is passed to innerHTML, the plain data is again given HTML capabilities. Following a four-step process of “serialization, HTML rendering, parsing, and DOM text output” for each title record, each step checks the actual API. This test is more effective than simply scanning for dangerous tags in Markdown, and can uncover cross-site vulnerabilities more effectively.
Replace Arbitrary HTML with a Structured Admonition Contract
Assume the author needs a “Migration Notes” field. The input should only contain the type, title, and controlled Markdown content, with the type restricted to “note,” “warning,” and “caution.” The component determines the label, class name, and accessible structure, and does not allow the author to provide event attributes, arbitrary styles, or component imports. This allows the author to express themselves, while the project retains its functionality. If a field is missing, a clear diagnostic message is returned instead of rendering the original content as HTML.
It is also necessary to specify whether nested prompts are allowed. Unlimited nesting would increase parsing and layout costs, and even legitimate input could put a strain on resources. Limiting the maximum depth, content length, and plugin capabilities is part of usability and security. The upper limit is based on editorial requirements and testing conditions, not a random number. If the limit is exceeded, inform the author on how to split the content, and avoid silently truncating code or displaying critical warnings.
For link components, the input is the address and the displayed text, and the protocol and target format are independently verified before output. For video components, the input is the supplier identifier, not the complete iframe, and a trusted component generates a fixed source. If the supplier adds new parameters, review their function first before expanding the schema. Do not re-open all restrictions using a single “other attributes” dictionary, otherwise, structured components are simply renamed arbitrary HTML.
Demonstrate normal functionality using positive examples: The warning title should be readable by the parser, the code should be copyable, and the links should have the expected addresses. Demonstrate boundary conditions using negative examples: Unknown component types, event fields, excessive nesting, and invalid addresses should be rejected. The final DOM resulting from the interaction between the component and the sanitizer must also be checked, and it is not sufficient to simply prove that the input object conforms to the specified type. Compile-time type checking controls developer input, while runtime validation controls the actual submission. The two have different scopes of application.
Preserving Semantic Differences Between Code and Text in Parse Trees
An article about security might need to display <script> or a dangerous protocol. When these appear in code nodes, they should be preserved as text. When they appear in actual links or original HTML nodes, they should be rejected according to the rules. Regular expressions are difficult to understand this context, and may either miss dangerous nodes or delete examples that readers need. First, parse them into a structured tree, and then process them based on the clear node types, which can preserve the writing semantics.
The class names and elements output by the highlighters should also be limited to the actual required sets. Allowing arbitrary style attributes can expand the presentation capabilities, and allowing arbitrary IDs can affect page anchors and script selection. It is possible to use a specific prefix for IDs generated in the main content to reduce the probability of conflicts with navigation or application controls, but it is still necessary to check for duplication and consistency. Prefixes are not a safe alternative, but only address a portion of the naming space issues.
HTML code that has been cleaned should not be passed to a plugin that re-parses and adds untrusted content. If post-processing is necessary, list its capabilities and re-validate at the final boundary. Testing can save a list of generated nodes for each stage, identifying which stage introduces dangerous properties, rather than just seeing that the final string is incorrect. This allows for precise determination of where changes originate – whether from parsing, transformation, or cleaning rules – when upgrading the plugin.
Resource Security Includes Build-Machine Consumption
Unreliable Markdown can cause rendering to take a long time, even without executing scripts, due to excessively long lines, complex nesting, or large charts. Public submission portals should limit input size, and tasks involving creation or rendering should have resource budgets and timeouts. If charting tools call external programs, file access and network capabilities should be isolated even more. You cannot assume that the generation process has no risks simply because the final result is an image.
When using a trusted repository, it’s also important to pay attention to dependency permissions. During plugin runtime, it may read files and environment variables. If the content author can modify the plugin configuration or package scripts, their permissions have exceeded the scope of the main content editing. Separate the submission materials and build code into different review boundaries, and do not inject production secrets during public contribution testing. Preview requirements can be met by isolating the build, without having to execute every external submission alongside production deployment credentials for convenience.
The result cache needs to include versions of the parser, plugins, and cleaning rules. After fixing the cleaning vulnerability, continuing to reuse old HTML will leave the original content in a state where it appears “unchanged,” while retaining the vulnerability. Content fingerprints can identify the input, while version conversion can identify the processed semantics. Together, these determine the output identity. Invalidation and rebuilding of all affected records should be part of the security update implementation.
Preview and Publication Must Not Use Contradictory Capabilities
If the preview allows HTML, and the final published version removes it, the author may only discover that key sections have been removed after publication. The private editing interface should clearly display the rejection diagnosis, using the same content rules as the public rendering. Development hot update scripts and other environment differences should be handled separately, not through simply opening the main text input. Preview permissions should control access, and rendering rules should control content capabilities. These are not the same switch.
Instead of just taking screenshots before publishing, compare the semantic nodes. Verify that important code blocks, links, and alert boxes remain, that alternative text is present, and that any legally sensitive content has not been accidentally removed. Visual screenshots can reveal layout issues, but they cannot prove that link protocols and attributes are secure. The final acceptance should check the structural, behavioral, and content integrity, allowing the author to restore normal expression without having to bypass the cleaning process.
Maintain the same origin boundary for other exports
The search index should extract plain text, rather than rendering the HTML directly and then injecting the result into a component. The full RSS content should use its own static output constraints, and social summaries should use text fields. The converter can share source state and approved content, but each output still performs its corresponding contextual encoding. This allows an article to obtain the ability to generate HTML content, without automatically gaining the same permissions in script data, attributes, or XML.
Check Published Copies After Fixing a Vulnerability
If you discover that older articles are vulnerable to injection, fix the renderer and rebuild all affected pages. Check if your CDN and RSS feeds are still providing the old output. Fixing only the new submission entry will not change the existing HTML. If necessary, clear the cache and review access logs, but copies saved by readers cannot be guaranteed to be deleted, so you must also revoke any corresponding credentials that may have been exposed.
Responsibility demarcation can be very clear: the source and author retain control over content input, the converter ensures legal grammar, the cleaner enforces permitted execution, the template handles contextual encoding, and deployment manages additional strategies and complete updates. No single layer can claim to replace any other. These responsibilities should be documented in testing and release checks, which transforms a seemingly simple text file into an auditable content processing chain.
References: OWASP XSS Prevention, HTML: Script Content Restrictions, MDX Documentation.