Skip to content

Rendered-output sanitization

This document describes the security measures core’s renderer applies to user-provided content when generating HTML output, and how to use ZiraDocs/DocLang safely. For how to report a security vulnerability, see SECURITY.md at the repo root.

All user-provided content is HTML-escaped before being rendered to prevent script injection:

& → &
< → &lt;
> → &gt;
" → &quot;
' → &#39;

URLs in links and images are validated to block dangerous protocols.

Blocked protocols: javascript:, data:, vbscript:, file:

Allowed protocols: http:, https:, mailto:, tel:, ftp:, and relative URLs (e.g. /path/to/resource).

HTML attributes (alt text, captions, data attributes) are sanitized to prevent attribute injection: newlines/carriage returns are removed, tabs are replaced with spaces, and HTML special characters are escaped.

Code blocks are HTML-escaped to prevent execution of embedded scripts while preserving syntax highlighting — e.g. a fenced ```javascript block containing <script>alert('x')</script> renders the text safely rather than executing it.

5. Renderer-authored inline formatting (fixed allowlist)

Section titled “5. Renderer-authored inline formatting (fixed allowlist)”

Inline formatting never passes user HTML through. The pipeline escapes the whole string first (section 1), then injects a fixed, renderer-authored set of tags via regex: <strong>, <em>, <mark>, <del>, <code>, and <a> (with URL validation per section 2). Class-based spans ([content]{.token}) extend this set with a few more renderer-authored tags — <span class="slidelang-text-*">, <mark class="slidelang-highlight-*">, <u>, <small> — but only via a fixed token→output allowlist (inlineSpanTokens in renderer/sanitizer.go). The captured token is used solely as a map key and is never interpolated into a class attribute; a token outside the allowlist (or one containing characters other than [a-zA-Z0-9-]) leaves the text literal and escaped, injecting nothing. This mirrors the SanitizeColor/cssNamedColors validate-against-a- fixed-map model and keeps the escape-everything-then-inject-known-tags invariant intact: the only markup the renderer can emit is from this closed set.

Automatically protected: text elements, tables (headers and cells), lists (items and sub-items), quotes (content/author/source), checklists, images (URL validation + attribute escaping), code blocks, special blocks (title/content/icon), code groups (labels/content), maps (titles/marker labels/attributes), charts (titles/labels), diagram titles, and template variables.

Mermaid and PlantUML diagrams. Diagram content is not HTML-escaped because these renderers require raw source syntax. Diagram titles are still sanitized, and diagrams render in isolated contexts (SVG or rasterized images via chromedp).

Raw JSON in charts. When using json: mode for charts, the JSON is validated as JSON but not HTML-escaped, since it’s consumed directly by Chart.js on the client. Only use trusted data sources for JSON-mode charts.

# Safe Heading
**Bold text** and *italic text* work safely.
This <script>alert('xss')</script> is escaped.
[Safe link](https://example.com)
[Blocked link](javascript:alert('xss')) <!-- blocked -->
![Safe image](https://example.com/image.png)
![Blocked](javascript:alert('xss')) <!-- blocked -->
---
title: My Presentation
user_input: "<script>alert('xss')</script>"
---
Text: {{user_input}} # rendered safely escaped
  • Validate untrusted input if you’re generating presentations/documents from user-submitted content, beyond what the renderer’s escaping already covers.
  • Be mindful of variable sources. Frontmatter/template variables are escaped automatically, but prefer trusted sources over raw unvalidated user input.
  • Keep dependencies up to date — pull the latest slidelang/doclang release to get security fixes.
  • Review generated HTML for sensitive presentations/documents before publishing it externally.

The sanitizer lives in core/renderer/sanitizer.go and is used by both the shared document renderer (renderer/document_html.go, used by doclang) and slidelang’s own slide generator (slidelang/internal/generator/). New rendered output should always go through it.