Security
Typlet renders formulas that pages often take from untrusted people: chat messages, comments, issues and shared links. This document describes what Typlet guarantees for such input, how the guarantees are tested, and what a page using Typlet still needs to do.
Reporting a vulnerability
If a formula can break one of the guarantees below, by putting markup into the output that Typlet didn't make, or by making a render run without bound, please report it privately: use Report a vulnerability on the repository's Security tab, and don't open a public issue. Include the formula, the options, and Typlet's version. The latest release gets the fix.
Threat model
An attacker controls the formula's source. They do not control the page's code, so Typlet's options, such as trust, errorColor, preamble and fallback, are trusted. Typlet aims to guarantee two things for any formula:
- No markup injection. The output contains only the elements and attributes Typlet makes, with no scripts, event handlers, styles from the formula, or links the page didn't allow.
- Bounded work. Rendering stops quickly with a
TypletErrorof kindlimit, whatever the formula computes.
Markup
Every output is built from escaped text and fixed element and attribute names:
- Text and attribute values are escaped. That covers the formula's text, its strings and raw text, equation numbers (a numbering function can return any content), error messages and the source kept in annotations and labels. Attribute values are always quoted.
- Element and attribute names never come from the formula. All outputs use 33 elements (
span,a,br,code,em,mark,p,strongand MathML's elements) and 28 attributes, listed intest/security.test.ts. - Styles hold only numbers, units and colors. Colors from
rgb,lumaand named colors are written as hexadecimal, and lengths as numbers inem. No string from the formula reaches astyleattribute. - Links need
trust.#linkdraws a link only where thetrustoption allows it. It isfalseby default,trueto trust every link, or a function of the URL and its protocol:trust: ({ protocol }) => protocol === 'https'. The protocol is read as browsers read it: leading spaces and control characters are skipped and tabs and newlines dropped, so" java\tscript:alert(1)"has the protocoljavascript. A URL whose scheme is malformed is never trusted. Without trust, the link's body shows without the link, and a warning names the URL. - Unknown content is refused, not passed through. Typlet has no raw HTML element, and features it doesn't support raise a
TypletErrorof kindunsupported.
test/security.test.ts renders hostile formulas in every output, inline and in display, with and without throwOnError. It parses each output as a browser does, and checks it against the lists of elements and attributes, the safe style characters, and the allowed links. It also renders 400 random formulas made of characters that matter to HTML, CSS, URLs and Typst.
Work
Formulas can compute: Typst's code has functions, recursion and growing values. Typlet bounds each formula's work (docs/DESIGN.md §6.6):
| Budget | Default | Option |
|---|---|---|
| Function calls | 30,000 | maxCalls |
| Call depth | 80, as in Typst | — |
| Parser nesting | 256, as in Typst | — |
| Expression depth | 1,024 nested expressions, counted across the function calls in progress, including the rest of a block after a rule and the body of a closure being defined | — |
| Nesting depth | 256 levels of content being resolved, laid out or converted, and of values being compared or shown | — |
| Value steps | 100,000 per formula, comparing and showing values | — |
| Items in an array or dictionary | 10,000 | maxCollectionSize |
| Characters in a string | 100,000 | maxStringLength |
| Content elements, and parts of stretched glyphs | 10,000 each | maxElements |
| Sizes of spacing, boxes and text, and every length in the output | 1,000 em | maxSize |
| Output | 1,000,000 characters | maxOutputSize |
The first three of the limits without an option bound recursion, which follows the formula's structure: a chain of thousands of operators, definitions that nest content thousands deep, a chain of attachments, a closure whose body is such a chain, a style that feeds an element its own body, such as #set box(box()), and values that share parts, such as (x, x) built thirty times over, all stop with a limit error rather than overflowing the native stack or taking exponential time. The parser follows a chain of else if branches in a loop, where typst-syntax recurses. Every length the HTML and the MathML carry is checked against the size budget as it is written, whatever computed it, so no gap, stroke, tracking or shift reaches the page beyond it.
npm run check:budgets checks that the built bundle stops each abusive formula in test/budget-cases.mjs within 50 ms: recursion bombs, repetition, content that doubles, deep nesting and huge sizes, chains of operators, attachments and else if branches, content nested by definitions, styles that feed content into itself, values that share parts, chains of ever taller delimiters, and huge lengths from gaps, strokes and tracking. It also checks that formulas whose work must stay linear in their size render within 50 ms, such as raw code tagged as Typst, which Typlet parses and highlights again, with a long chain of field accesses. Typlet has no loops, file access, packages, eval or introspection (docs/DESIGN.md §6.3), so no formula reaches outside its own evaluation.
Differential fuzzing
Two fuzzers compare Typlet with the real Typst compiler, through the oracle in oracle/:
npm run fuzzparses random inputs with Typlet and typst-syntax, and compares their syntax trees.npm run fuzz:evalrenders random formulas from a grammar of math and embedded code, with and without preambles. It compares Typlet's MathML, diagnostics and layout with Typst's. Every render, in every output, must finish within a second or fail with aTypletError.
Before the 0.1.0 release, 40,000 formulas of fuzz:eval, seeds 20 to 27, and 100,000 inputs of fuzz rendered with no divergence, crash or hang. Earlier sweeps found divergences in how Typst's HTML export treats content that isn't math, in whitespace, paragraphs, smart quotes and boxes, and in tracking, which are now fixed, and kept as tests.
What pages need to do
- Keep
trustoff for untrusted formulas, or allow only the protocols you want, such ashttps. - Content Security Policy. Typlet's HTML uses inline
styleattributes, as KaTeX's does, so a policy needsstyle-src 'unsafe-inline', orstyle-src-attr 'unsafe-inline'where supported. Typlet runs noevaland makes no requests; the fonts come from your owntyplet.css. - A
fallbackthat renders refused formulas runs other code, such as Typst compiled to WebAssembly, on the same untrusted source. Its output is inserted as it is, so it must be safe too. - Budgets can be lowered for pages that render many formulas, such as a chat log, so that one message can't take more than its share.