- modules/breakwater exports Render(markdown) and Purify(html): goldmark GFM in its default safe mode (raw HTML stripped, unsafe links dropped), then a named bluemonday policy built from NewPolicy, not the UGC preset - the policy is the PHP golem15/forum HTMLPurifier config 1:1: the fixed element list, a[href|rel|target], http/https/mailto only, rel=nofollow on every link, target=_blank plus noopener on fully-qualified links - tests cover the PHP ForumMarkdownRenderTest corpus, links per scheme, blank input, the Purify allow-list and concurrent use; testable examples feed the docs page snippets - module README, root module table row, introduction.md Utilities row and docs/services/markdown.md (which gate serves user content vs admin fields) - go.mod and go.sum unchanged: bluemonday v1.0.27 and goldmark v1.8.6 were already pinned
3.6 KiB
title, description, section, order
| title | description | section | order |
|---|---|---|---|
| Markdown and HTML sanitizing | Render user Markdown to safe HTML and sanitize stored HTML with the breakwater allow-list; which gate serves admin fields instead. | services | 165 |
Markdown and HTML sanitizing
A plugin that stores content a site user submits, such as forum posts, comments or HTML imported from another system, serves that content on a public page. breakwater is the one gate it goes through: breakwater.Render turns Markdown into HTML and breakwater.Purify sanitizes content that is already HTML, both with the same fixed allow-list. The plugin calls the gate on write, stores the result and serves it as is.
The two calls
// A forum post: store the rendered HTML on write and serve it as is.
post := "See [the docs](https://example.com/docs) and <b onclick=\"x()\">this</b>.\n\n<script>alert(1)</script>"
html := breakwater.Render(post)
fmt.Println(html)
// Output:
// <p>See <a href="https://example.com/docs" rel="nofollow noopener" target="_blank">the docs</a> and this.</p>
// Content stored as HTML: the same allow-list, no Markdown pass.
stored := `<p onclick="x()">Hi <b>there</b><img src="a.jpg"></p><a href="tel:+48">call</a>`
clean := breakwater.Purify(stored)
fmt.Println(clean)
// Output:
// <p>Hi <b>there</b></p>call
breakwater.Render is GitHub Flavored Markdown: tables, fenced code, strikethrough, links and autolinks render; breakwater.Purify is the policy alone, for content a plugin keeps as HTML. Neither returns an error: both return the policy's output, and "" for blank or whitespace-only input.
The policy
The allow-list is the PHP golem15/forum HTMLPurifier config, 1:1.
| Rule | Value |
|---|---|
| Allowed elements | p, br, strong, em, b, i, h1–h6, ul, ol, li, blockquote, pre, code, a, table, thead, tbody, tr, th, td, hr, del, ins, sub, sup. Everything else is removed, including img, figure, script, style, iframe, form, svg and every on* attribute; class and id are dropped from the elements that stay. |
a attributes |
href, rel and target. A caller-supplied rel or target with a value the policy would not write itself is dropped and rewritten. |
| URL schemes | http, https and mailto. No tel: and no relative URLs; a link whose href is refused loses its tag and keeps its text. |
| Link rewriting | Every link gets rel="nofollow"; every fully-qualified http or https link also gets target="_blank" and rel="noopener". |
Raw HTML in Markdown is stripped, not escaped into visible text: text <b onclick="x()">bold</b> more renders to <p>text bold more</p>, with neither the tag nor an <b> in the output. HTML comments are removed. A Markdown link to a javascript:, data: or vbscript: destination is dropped before the policy runs, and its text stays.
Which gate
breakwaterfor user content: anything a site user submits that a plugin stores and serves, in Markdown or HTML.- seaglass for administrator rich text saved through cabana's
richeditorfield (see Forms): bluemonday's UGC policy, which keeps images, relative andtel:links, and links as written withoutnofollow.seaglass.Sanitizeis not a gate for user content. cabana.RenderMarkdownfor the admin Markdown field preview only: CommonMark without GFM behind a regex gate that refuses unsafe output instead of removing it.