Skip to content
All projects

03Portfolio

WireMark — a secure markup language

Extended Markdown with security built into the core: a tokenizer, parser, sanitizer and HTML renderer in TypeScript with no external dependencies, a NestJS integration, a WYSIWYG editor and a demo page. Anything not on the allowlist does not get through.

By the numbers

Element types in the tokenizer
24
External dependencies in the core
0
Input limit (characters)
100 000
01

Problem

User-written content — comments, descriptions, posts — needs more than plain text: headings, lists, code, highlights. But every such field is an opening for XSS: a script in the content, a javascript: link, an onerror attribute on an image, or invisible Unicode characters that flip text direction. Filtering finished HTML after the fact makes it easy to miss one case.

02

Approach

Instead of cleaning HTML, WireMark does not accept it at all: input goes through the sanitizer (100,000-character limit, NFC normalisation, removal of BiDi control characters and null bytes), then the tokenizer recognises 24 element types and the renderer builds HTML from those alone. All text is entity-encoded (& < > " ' /), links pass through an allowlist of protocols (https, http, mailto) and an optional domain list, images are https-only, and badge colours, callout types, code languages and emoji names each have their own allowlist. Alongside the HTML, the parser returns a security audit log and a suspicious-content flag, and validate() checks input before it is stored. For NestJS there is a module with forRoot and forFeature, a service, a pipe and a parameter decorator.

03

Outcome

The result is the library core (about 970 lines of TypeScript in four modules, with no external dependencies), a NestJS integration and two pages in plain HTML and JS: a demo with a live editor and preview, and a WYSIWYG editor with a “/” command menu, a floating formatting toolbar, an outline and export to HTML, WireMark or plain text. In the demo, an injected <script>, an image with onerror and a javascript: link are shown as text or blocked, and the audit bar lists what was rejected.

How it works

  1. 01

    Sanitizer first, syntax second

    Before anything is parsed, input is capped at a length limit, normalised to NFC and stripped of text-direction control characters, null bytes and mixed line endings.

  2. 02

    Allowlists instead of blocklists

    A link only gets through with https, http or mailto (and optionally from an allowed domain), an image only over https. An unknown badge colour falls back to grey, an unknown callout type to the default, and a code-block language is trimmed to letters, digits, dash and underscore.

  3. 03

    An injection stays text

    All text is entity-encoded, so <script> or <img onerror> are displayed as characters rather than executed. javascript: links are rejected, and the parser records every such event in the audit log.

    An injection stays text
  4. 04

    Extended syntax

    Beyond Markdown: typed callouts, badges with colour and variant, spoilers, highlights, superscript and subscript, emoji by name from a closed list, and code blocks with a copy button.

    Extended syntax
  5. 05

    Ready for NestJS

    A WireMark module configured with forRoot or forFeature, a service for manual parsing, a pipe for request fields and a parameter decorator that returns safe HTML straight away.

Write