Markdown without a back door: <md>

Markdown without a back door: <md>

BackerLeader ●1 ●12 ●51
calendar_today ago • schedule5 min read

How scrml lets an author write bold instead of , and why nothing inside an block can ever run.

Where this came from
It started with a security review, not a feature request. scrml treats the bodies of

 and  as raw content, and until this week the compiler copied raw content into the page exactly as written. A review then ran these through a real Chromium:
image // the handler runs
// a same-origin script runs
// the CSS applies

The fix was a ruling: raw bodies now render as text. < and > are encoded, & is left alone so the many existing <…> code samples keep working, and the common

…
wrapper stays a real element. Across 4,523 raw bodies in 12 repositories, that breaks nothing.

It also closed the last place an author could type formatting tags inside text. so I asked the obvious question: can scrml have a first-class , so an author gets the effect of bold without writing ?

The answer is yes. It took four decisions.

What it looks like
You write

<md>
    ## Filing a good bug report

    Before you file, **search the board**.

    - Say what you *expected*.
    - Paste the exact error in `backticks`.
    - Read the [triage guide](/docs/triage).
</md>


The page gets

…


Before you file,
search the board.


  • Say what you expected.
  • … backticks.
  • … …

The body is dedented the way Java text blocks are: the smallest indentation among the lines, counting the closing tag's line, is stripped. So sits naturally inside indented scrml markup. Static is turned into elements at build time and costs nothing at runtime.

DECISION 1 of 4

A value inside is text, never markdown
Ruled: ${…} inside is text-only.

## Welcome back, ${@user.name} Your last order shipped **${@order.date}**.

Now suppose a user sets their name to this:

x

Your account is locked

Verify here
If interpolated values were parsed as markdown, that name would render a fake heading and a phishing link. The link is a perfectly valid https URL, so no URL check would stop it. Under the rule we chose, the value goes in as plain text and can never create structure. The page shows the odd name literally.

This is the same contract scrml already uses for SQL. In ?{ … WHERE id = ${id} } the value is a bound parameter: data is never code. applies that rule to prose.

DECISION 2 of 4

Markdown from data gets its own, explicit form
Ruled: is part of , designed now and built second.

Comments, posts and CMS rows hold markdown that arrives at runtime. That is the dangerous case, so it does not happen by accident. It has its own spelling:

= ?{ SELECT body FROM posts WHERE id = ${id} }.get()


It uses the same reader and the same small set of output elements as static . A user's markdown can produce bold and a link, but never a script, an event handler or a javascript: link. Data-driven headings get no generated id, because ids chosen by users can overwrite DOM globals. The parser for this form ships only on pages that use of=; it is estimated at 3–6 KB gzipped, an estimate that has not been measured yet.

DECISION 3 of 4

A small dialect, one way to write each thing
bryan: "one way to do it with lints to guide"

In
paragraphs

headings

em and strong
inline code and fenced code
inline links and images
lists and > quotes
--- rules
tables and strike
Out
raw HTML (shown as text)
indented code blocks
underlined === headings
reference-style links
entity references
https://… autolinks
footnotes and task lists
Markdown has several ways to write most things. scrml keeps one. When pasted markdown uses a dropped construct, a W-MD-* warning names the form to use instead, so a cut teaches rather than surprises.

Some cuts are forced. Indented code blocks had to go: lives inside indented source, and an indentation-sensitive construct would misfire constantly. MDX dropped them for the same reason. Others make the dialect safer. Raw HTML is the obvious one, and task lists are out because they would put form controls into the output. Djot, John MacFarlane's cleanup of CommonMark, made the same kinds of cuts.

DECISION 4 of 4

One markdown reader, written in scrml
Ruled: the parser is a single scrml module, scrml:md, used by both compilers.

scrml currently has two compilers: impl#1, written in TypeScript, and the self-hosting bootstrap, written in scrml. The markdown parser is written once, as an ordinary scrml module. impl#1 compiles it like any other stdlib module and uses it twice: at build time to turn static into elements, and as the runtime parser shipped for . The bootstrap uses the same module.

The alternatives were a TypeScript parser now and a scrml one later, or waiting for the bootstrap. The first means two readers of the same text, and two readers that can disagree is exactly the shape that kept producing security bypasses this session. The second means waiting. One reader avoids both. The cost is new machinery: impl#1 needs a hook that runs a scrml module at compile time. That hook gets the same adversarial review as everything else.

Why nothing inside can run
Most markdown renderers produce an HTML string and then clean it, by stripping , blocking javascript:, and so on. Those are lists of known-bad things, and the list is always one entry short. DOMPurify has a long run of mutation-XSS bypasses because what it checks and what the browser re-parses can differ. @nuxtjs/mdc had a CVE (CVE-2025-24981) for javascript: URLs hidden behind hex entities.

works the other way around. The renderer can only build a fixed set of elements, each with a fixed set of attributes: ElementAttributesWhere the value comes from p em strong del blockquote ul li hr br table thead tbody tr prenone— olstartdigits from the list marker th tdstyleone of three compiler constants for alignment h1–h6ida compiler slug, static only codeclasslanguage- plus letters, digits, _+- ahref, titlehref must pass scrml's URL allow-list imgsrc, alt, titlesrc must pass the same allow-list Content reaches the page only as text nodes or as values of those attributes. No HTML string is ever handed to the browser to parse. The URL check is scrml's existing allow-list, which admits http:, https:, mailto: and similar schemes rather than trying to name every dangerous one. A dangerous element the browsers add next year cannot appear, because has no way to build it. Who pays Authors can't put a
or inside . They close the block and write the element in ordinary scrml markup. People pasting from GitHub see raw HTML tags as text, and get a warning for each dropped construct. Pages that render user markdown ship the parser, estimated at 3–6 KB gzipped. Pages with only static ship nothing. The compiler gains a compile-time hook into a scrml module, which is new machinery to get right. Not covered: a comment rendered through of= can still say "click here to re-verify" with a real https link. That is content moderation, and true of every forum. Images in user markdown can also act as tracking pixels; an opt-in image policy is still an open question. Prior art The closest designs are Markdoc, from Stripe, which keeps content declarative and won't mix arbitrary code into it, and elm-markdown, which renders to a typed tree instead of an HTML string and fails loudly on unexpected HTML. Djot shaped the dialect cuts. Most static-site tools, such as Hugo and markdown-it, default to dropping raw HTML. Tools that pass it through, such as marked, leave sanitizing to the user; marked's README says so directly. Status: the four decisions were ruled in S462. The full design, including the prior-art table and the security model, is in scrml-support/docs/deep-dives/md-element-2026-10-11.md. Build phases: 1 is the SPEC section and the scrml:md parser (in progress); 2 is the impl#1 hook for static ; 3 is . This draft should be re-checked against the compiler once phase 2 lands, and every code block compiled before publishing.
🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelski - Mar 19

If You Give a Dev an Enum, then

Bryan MacLeeverified - Aug 31

A struct is a data strucure, of course of course

Bryan MacLeeverified - Aug 26

I have a conffession to make

Bryan MacLeeverified - Aug 26

Is there any way to make type conversion sexy? Nope

Bryan MacLeeverified - Oct 20
chevron_left
3.8k Points • 64 Badges
Vernal UT • scrml.dev
17Posts
38Comments
56Connections
I am a truck drive specializing in the oil and gas industry, who happens to love coding.
i have been... Show more

Related Jobs

View all jobs →

Commenters (This Week)

3 comments
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!