Documentation
¶
Overview ¶
Package security provides the opt-in security toolkit for Fluent. It covers the two jobs a rendering framework cannot honestly tackle with a small built-in helper: sanitising untrusted HTML, and coordinating Content-Security-Policy nonces with the markup Fluent emits.
Why this lives outside Fluent ¶
Fluent's core is pure rendering with zero external dependencies, and that is a property worth preserving. Real HTML sanitisation needs a proper parser and a vetted policy engine; attempting it with a regex inside the framework would have been dishonest. Pulling security out into its own package lets Fluent stay small while making security features first-class for the projects that want them.
What is in here ¶
- HTML and PlainText - one-liner helpers for the common case, returning a Fluent node.Node directly.
- Cleaner with New, RichText, and FromPolicy constructors plus chainable Cleaner.Allow, Cleaner.AllowClasses, Cleaner.AllowAttr methods for building custom policies without dropping into bluemonday's configuration surface.
- Nonce - a Content-Security-Policy nonce generator. Pair it with [script.Nonce]/[style.Nonce] on the rendering side and a matching 'nonce-<value>' entry in the CSP header.
What is not in here ¶
Ad-hoc pattern scanning. An earlier iteration of Fluent shipped a regex-based "validator"; it was removed because it promised safety it could not deliver. A motivated attacker defeats any fixed pattern list via Unicode escapes, string concatenation, or HTML entities. Sanitisation goes through a real parser; inline script and style safety goes through CSP. There is no third path.
Choosing a helper ¶
- HTML: one-off call for rich-text content (rendered markdown, rich-text editor output). Uses a permissive-but-safe baseline.
- PlainText: one-off call when you want the text content without any surviving markup.
- RichText: same baseline as HTML but returns a Cleaner you can chain more configuration onto. Use when one preset fits the whole app and you want to define it once.
- New: empty-allowlist Cleaner to build a narrow custom policy from scratch.
- FromPolicy: escape hatch when you already have a bluemonday policy and just want to wrap it as a Cleaner.
Under the hood ¶
The current implementation is backed by bluemonday. That is an implementation detail: the package API talks in terms of "allow these elements" and "sanitise this HTML," not in terms of bluemonday's policy types. If a better sanitiser emerges, callers who used only the chainable API will need no changes; only users of FromPolicy would feel the swap.
Example: sanitised user content ¶
// One-off: a comment body pasted in from a rich-text editor.
comment := div.New(
h3.Text(user.Name),
div.New(security.HTML(user.CommentHTML)).Class("body"),
).Class("comment")
// Reusable: hoist a cleaner with a tweaked policy at package
// scope, then share it.
var mdCleaner = security.RichText().AllowClasses("code", "pre")
body := div.New(mdCleaner.Clean(article.RenderedHTML)).Class("article-body")
Example: inline script with CSP nonce ¶
nonce := security.Nonce()
w.Header().Set(
"Content-Security-Policy",
fmt.Sprintf("script-src 'self' 'nonce-%s'; style-src 'self' 'nonce-%s'",
nonce, nonce),
)
page := html.New(
head.New(
script.RawText("console.log('hello')").Nonce(nonce),
),
body.New( /* ... */ ),
)
page.Render(w)
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func HTML ¶
HTML sanitises untrusted HTML and returns a node.Node ready to embed in a Fluent tree. Uses the rich-text baseline (common formatting tags, links limited to http/https, scripts and event handlers removed). For sanitising the same class of content in more than one place, build a Cleaner with RichText and reuse it so the policy appears once in your code.
Use this for rendered markdown, rich-text editor output, comment bodies - any content where the user is expected to supply some HTML structure and you want formatting to survive.
The returned node renders the sanitised output verbatim. Fluent does not HTML-escape it again, because the sanitiser has already guaranteed the output is safe.
func Nonce ¶
func Nonce() string
Nonce returns a fresh 128-bit random value encoded as URL-safe base64 without padding (22 characters). Generate a new nonce per HTTP request and use it in two matching places:
- As the `nonce` attribute on any inline <script> or <style> element you want the browser to execute - stamp it via script.Nonce(n) or style.Nonce(n) on the Fluent side.
- As 'nonce-<value>' under the matching directive (script-src or style-src) in the Content-Security-Policy header.
The browser will only execute inline blocks whose nonce attribute matches the value declared in the header. Nonces must never be reused across requests. Reuse does not make this nonce easier to predict (128 bits of crypto/rand remain unpredictable), but it removes the freshness property CSP relies on: once a nonce is observed on the wire or in the rendered DOM, any later injected content on a page that shares that nonce will be allowed to run.
Nonce matches the stdlib's approach to entropy failure. crypto/rand.Read is documented (Go 1.24+) to never return an error and to always fill its buffer; on entropy-source failure it panics internally rather than surfacing a recoverable error. Nonce mirrors that model: it returns a plain string, not (string, error), because no HTTP handler has a useful response to "we could not produce secure randomness" other than to stop. The explicit err check below is kept as a defensive assertion against a future loosening of that stdlib contract - if it ever fires, panicking remains the only safe response, since a predictable nonce silently defeats CSP.
func PlainText ¶
PlainText strips all HTML tags from input and returns a node.Node that renders only the surviving text content. Use when markup survival would be a bug - for example, rendering a user-provided title, a one-line summary, or any display context where HTML structure is not wanted.
The surviving special characters (`<`, `>`, `&`, `"`, `'`) are already escaped to HTML entities by the sanitiser before return. The node therefore renders its content verbatim; passing it through Fluent's Text() (which would escape again) would double-encode those entities - for example `<` would become `&lt;`. HTML helpers in this package always use RawText internally so callers do not have to know this.
Types ¶
type Cleaner ¶
type Cleaner struct {
// contains filtered or unexported fields
}
Cleaner is a configurable sanitiser. Construct one with New for an empty allowlist, RichText for the UGC-style baseline, or FromPolicy when you already have a bluemonday policy. Customise with the chainable Cleaner.Allow, Cleaner.AllowClasses, Cleaner.AllowAttr methods. Cleaner.Clean produces a sanitised node.Node for embedding in a Fluent tree.
Cleaner is safe for concurrent use once configuration is complete. Building (calling the Allow* methods) and calling Clean from multiple goroutines simultaneously is not supported - configure fully, then share.
func FromPolicy ¶
func FromPolicy(policy *bluemonday.Policy) *Cleaner
FromPolicy wraps an existing bluemonday policy in a Cleaner. Use this when the built-in presets and chainable methods do not cover your case and you need bluemonday's full configuration surface. Most apps will not reach for this - prefer New or RichText followed by the Allow helpers, so the bluemonday dependency stays an implementation detail rather than a cross-package concern.
func New ¶
func New() *Cleaner
New returns a Cleaner with an empty allowlist. Nothing is permitted until you call Cleaner.Allow, Cleaner.AllowClasses, or Cleaner.AllowAttr. Use this when you want to define a narrow policy from scratch.
For most apps, RichText is the better starting point - it comes pre-configured with the widely-accepted UGC tag set (paragraphs, headings, lists, links with http/https, inline code, emphasis, and similar) and you chain on what you need to add.
func RichText ¶
func RichText() *Cleaner
RichText returns a Cleaner pre-configured with the user-generated-content baseline: the common formatting tags survive, scripts and event handlers are stripped, and links are limited to http/https schemes. Chain Cleaner.Allow / Cleaner.AllowClasses / Cleaner.AllowAttr to extend; there is currently no method to narrow the baseline - for that, start from New instead.
The underlying policy is bluemonday's UGCPolicy. Fluent-security treats that as an implementation detail: the guarantees to rely on are "common HTML formatting allowed, known attack surfaces removed," not any specific bluemonday version's exact tag list.
func (*Cleaner) Allow ¶
Allow permits the named elements. Tags not on the allowlist (whether added here or seeded by the constructor) are stripped at sanitise time. Chaining Allow repeatedly is fine - allowlist additions are idempotent.
security.New().Allow("b", "i", "em")
func (*Cleaner) AllowAttr ¶
AllowAttr permits the named attribute on the named elements. Use this for attributes other than class; for class specifically prefer Cleaner.AllowClasses as it reads more naturally at the call site.
security.RichText().AllowAttr("id", "h1", "h2", "h3")
func (*Cleaner) AllowClasses ¶
AllowClasses permits the class attribute on the named elements. This is the common case for preserving syntax-highlight hooks on <code>/<pre> output from markdown renderers or code-display components.
security.RichText().AllowClasses("code", "pre")