Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,28 @@

mojo-markdown is a pure-Mojo CommonMark parser and HTML renderer with no
network access, no authentication, and no secrets handling — it reads
markdown text and returns rendered HTML or an AST. The main risk
surface is malformed or adversarial input causing a crash or hang.
markdown text and returns rendered HTML or an AST.

## Output is NOT sanitized

Per the [CommonMark spec](https://spec.commonmark.org/), raw inline and
block HTML, as well as `javascript:`, `data:`, and other arbitrary URL
schemes in links and images, pass through to the output **verbatim**.
mojo-markdown does **not** sanitize its output. Rendering untrusted
markdown and inserting the result into a live page is a stored-XSS risk:
an author can embed `<script>`, event-handler attributes, or a
`javascript:` link and it will be emitted unchanged.

If you render markdown from untrusted sources, run the HTML through a
dedicated sanitizer/allow-list (e.g. an equivalent of DOMPurify) before
displaying it. This library deliberately preserves spec-compliant
passthrough rather than sanitizing, so sanitization is the caller's
responsibility.

## Reporting crashes and hangs

Beyond the passthrough behavior above, the remaining risk surface is
malformed or adversarial input causing a crash or hang.

If you find an input that crashes, hangs, or otherwise misbehaves in a
way that looks security-relevant (e.g. out-of-bounds access, unbounded
Expand Down
25 changes: 21 additions & 4 deletions src/markdown/inline.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -398,9 +398,12 @@ def _parse_inline_suffix(s: String, lparen: Int) -> _LinkSuffix:
def _strip_tags(html: String) -> String:
"""Remove HTML tags, leaving text content (for image alt attributes).

Operates on already-escaped inline HTML, so `<`/`>` only appear as tag
delimiters and attribute values never contain a raw `>`. A nested
`<img>` contributes its `alt` text rather than vanishing.
Operates on inline HTML. Raw inline HTML passes through verbatim, so an
attribute value *can* contain a raw `>` (e.g. `<span title="a>b">`). The
tag scan is therefore quote-aware: it only treats a `>` outside a quoted
attribute as the tag terminator, so such a `>` cannot break out of the
surrounding `alt="…"` attribute. A nested `<img>` contributes its `alt`
text rather than vanishing.
"""
var bytes = html.as_bytes()
var n = len(bytes)
Expand All @@ -409,8 +412,22 @@ def _strip_tags(html: String) -> String:
while i < n:
var b = bytes[i]
if b == LT:
# Scan to the tag's closing `>`, skipping over quoted attribute
# values so a raw `>` inside an attribute does not end the tag
# early.
var j = i + 1
while j < n and bytes[j] != GT:
var in_quote = False
var quote_ch = UInt8(0)
while j < n:
var c = bytes[j]
if in_quote:
if c == quote_ch:
in_quote = False
elif c == QUOTE or c == SQUOTE:
in_quote = True
quote_ch = c
elif c == GT:
break
j += 1
# A nested image keeps its alt text.
if _match_bytes(bytes, i + 1, "img ") or _match_bytes(
Expand Down
22 changes: 22 additions & 0 deletions test/test_markdown.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,28 @@ def test_image_alt_is_plain_text() raises:
)


def test_image_alt_raw_html_gt_no_breakout() raises:
# A raw `>` inside a quoted attribute of raw HTML in the image
# description must not terminate the tag early and break out of the
# generated alt="…" attribute. Raw HTML contributes no alt text, so
# only the trailing literal `x` survives.
assert_equal(
render_html(String('![<span title="a>b">x](/i.png)')),
'<p><img src="/i.png" alt="x" /></p>\n',
)
# Single-quoted attribute value is handled the same way.
assert_equal(
render_html(String("![<span title='a>b'>x](/i.png)")),
'<p><img src="/i.png" alt="x" /></p>\n',
)
# A nested <img> still contributes its alt text, even when a later
# attribute contains a raw `>`.
assert_equal(
render_html(String('![<img alt="D" title="a>b">z](/i.png)')),
'<p><img src="/i.png" alt="Dz" /></p>\n',
)


def test_autolink() raises:
assert_equal(
render_html("<https://example.com>"),
Expand Down
Loading