# Security

Markdown rendered in a browser is often untrusted: comments, chat messages,
CMS content, LLM output. This page lists what mdlib-pure guarantees by
default and what depends on your configuration.

## Defaults

| Threat | Default behavior |
|---|---|
| Raw HTML (`<script>`, `<img onerror>`) | `html: false`: HTML is **not recognized**. It renders as escaped text, and Markdown around it still works. |
| `javascript:`/`vbscript:`/`data:` URLs | Every `href`/`src` goes through `allowUrl` (default `isSafeUrl`). Rejected URLs become `""`. Entity and percent-encoding tricks are decoded or neutralized before the check. |
| Markup injection through text | Text is escaped by the HTML serializer; `renderTo()` uses `createTextNode`/`setAttribute`. |
| Prototype pollution / lookup | Lookups keyed by input (entities, renderers, classes) use `Map` or `Object.hasOwn`. |
| Denial of service (CPU) | Parsing is linear in input size. `test/pathological.test.ts` runs 30+ adversarial inputs (nested emphasis and brackets, unclosed constructs, ReDoS-shaped strings, GFM tables and autolinks) with time limits. |
| Denial of service (stack) | Container and inline nesting are capped at `MAX_NESTING` (100); deeper markers render as text. |
| Output amplification | GFM table rows can add at most 524,288 empty cells in total. |

## When raw HTML is enabled

`html: true` passes raw HTML through **unchanged**. Use it only for trusted
input, or with a sanitizer:

```js
import DOMPurify from 'dompurify'
createMarkdown({ html: true, sanitize: DOMPurify.sanitize })
```

- `render()` applies `sanitize` to the final HTML string.
- `toVNodes()` (and so the React/Vue/Preact adapter) applies `sanitize` to
  every raw HTML node: each fragment reaching `dangerouslySetInnerHTML` or
  `innerHTML` has been sanitized.
- `renderTo()` builds DOM nodes directly when the tree has no raw HTML.
  With raw HTML (or a plugin emitting `r.raw()`), it serializes and assigns
  `innerHTML`, after `sanitize`. This is the only `innerHTML` in the library
  besides the entity decoder (which only ever receives `[a-z0-9]` names).
- The GFM plugin includes **tagfilter**: `<script>`, `<iframe>`, `<style>`,
  `<textarea>`, `<title>`, `<xmp>`, `<noembed>`, `<noframes>` and
  `<plaintext>` are neutralized. It is a GitHub-compatible filter, **not a
  sanitizer**: event handler attributes survive it.

## URL policy

The default `isSafeUrl(url, kind)` is an **allowlist**:

| URL | Links | Images |
|---|---|---|
| relative (`/a`, `a/b`, `#frag`, `?q`, `//host/path`) | ✓ | ✓ |
| `http:`, `https:` | ✓ | ✓ |
| `mailto:`, `tel:`, `sms:` | ✓ | ✓ |
| `data:image/png`, `gif`, `jpeg`, `webp` | ✗ | ✓ |
| `data:image/svg+xml` (can carry scripts), any other `data:` | ✗ | ✗ |
| everything else (`javascript:`, `vbscript:`, `file:`, `ftp:`, app links…) | ✗ | ✗ |

Rejected URLs render as `href=""` / `src=""`: the link text stays visible.
Note that GFM autolinks like `ftp://host` are therefore not clickable by
default.

Replace or extend the policy:

```js
import { createMarkdown, isSafeUrl } from 'mdlib-pure'

createMarkdown({
  allowUrl: (url, kind) => isSafeUrl(url, kind) || /^vscode:/i.test(url),
})
```

## Mermaid diagrams

The `mermaid` plugin itself only outputs escaped text. The SVG is created
and inserted by Mermaid, outside mdlib-pure's DOM-safe path, so its safety
is Mermaid's: `renderMermaid` uses `securityLevel: 'strict'` by default
(labels sanitized, no click callbacks); for untrusted input consider
`'sandbox'` and keep Mermaid up to date.

## Web Components

`<mdlib-markdown>` and `<mdlib-editor>` (`mdlib-pure/element`) render through
`renderTo()` with the defaults above, the editor's live preview included.

- Attributes only choose plugins and a named config. `html`, `sanitize`,
  `allowUrl` and `highlight` come only from `defineElements({ configs })`,
  written in JavaScript: markup or content that sets attributes cannot turn
  raw HTML on or loosen the URL policy.
- Math is typeset by KaTeX with its defaults (`trust: false`: no `\href`, no
  `\includegraphics`); diagrams by Mermaid with `securityLevel: 'strict'`.
  Both write into the element, outside mdlib-pure's DOM-safe path.
- The loaders are yours: `mermaid` and `katex` import what you give them, and
  `katexStylesheet` is fetched and added to `document.adoptedStyleSheets`.
  Point them at files you serve (no CDN is hard-coded).
- Framework wrappers built on the elements add their own notes:
  [Blazor](blazor.md#security),
  [Angular](angular.md#security).

## DOM clobbering

Element ids become globals (`window.config`), so ids derived from content
could let a document shadow names your scripts read. The `headings` plugin
produces clean ids by default (`#install`); for untrusted input, prefix them
as GitHub does: `headings({ prefix: 'user-content-' })`. GFM footnote ids
are already prefixed with `fn-`/`fnref-`.

## Plugins

Plugins run with full trust. A plugin must:

- pass URLs through `r.url(url, kind)`;
- return text as plain strings (escaped by the serializer), never build HTML
  strings from input;
- use `r.raw()` only for HTML it fully controls;
- keep its parsing linear (see rule 8 in `AGENTS.md`).

## Reporting

Report vulnerabilities privately to the maintainer (Michele Polo) rather than
in public issues.
