HTML Sanitization

The html module sanitizes untrusted HTML with policies based on bluemonday.

Sanitization parses an HTML fragment and filters it through an allowlist policy. Elements and attributes that the policy does not allow are removed, and the remaining fragment is normalized during serialization.

This is an API reference. Constructor blocks are self-contained policy snippets; later method blocks are partial configuration snippets that assume policy is an already-created policy. Sanitized output is suitable only for an HTML element-content context. It is not safe for JavaScript, CSS, URL, or HTML attribute interpolation; use an encoder for the actual output context.

Loading

local html = require("html")

Add html to the executable entry's modules: list before requiring it.

Preset Policies

The module provides three preset policy constructors:

Policy Use Case Allows
new_policy Custom sanitization Nothing (build from scratch)
ugc_policy User comments, forums Common formatting (p, b, i, a, lists, etc.)
strict_policy Plain text extraction Nothing (strips all HTML)

All three constructors return Policy, nil; policy construction does not currently fail.

Empty Policy

Create an empty policy, then add the elements and attributes it should allow:

local policy, err = html.sanitize.new_policy()
if err then return nil, err end

policy:allow_elements("p", "strong", "em")
policy:allow_attrs("class"):globally()

local clean = policy:sanitize(user_input)

Returns: Policy, error

User Content Policy

Create a policy configured for common user-generated formatting:

local policy, err = html.sanitize.ugc_policy()
if err then return nil, err end

local safe = policy:sanitize('<p>Hello <strong>world</strong></p>')
-- '<p>Hello <strong>world</strong></p>'

local xss = policy:sanitize('<p>Hello <script>alert("xss")</script></p>')
-- '<p>Hello </p>'

Returns: Policy, error

Strict Policy

Create a strict policy that removes HTML and returns plain text:

local policy, err = html.sanitize.strict_policy()
if err then return nil, err end

local text = policy:sanitize('<p>Hello <b>world</b>!</p>')
-- 'Hello world!'

Returns: Policy, error

Element Control

Allow Elements

Allow specific HTML elements:

local policy, err = html.sanitize.new_policy()
if err then return nil, err end
policy:allow_elements("p", "strong", "em", "br")
policy:allow_elements("h1", "h2", "h3")
policy:allow_elements("a", "img")

local result = policy:sanitize('<p>Hello <strong>world</strong></p>')
-- '<p>Hello <strong>world</strong></p>'
Parameter Type Description
... string Element tag names

Returns: Policy

Attribute Control

Allow Attributes

Start an attribute rule, then apply it with on_elements() or globally():

policy:allow_attrs("href"):on_elements("a")
policy:allow_attrs("src", "alt"):on_elements("img")
policy:allow_attrs("class", "id"):globally()
Parameter Type Description
... string Attribute names

Returns: AttrBuilder

On Specific Elements

Allow attributes only on specified elements:

policy:allow_elements("a", "img")
policy:allow_attrs("href", "target"):on_elements("a")
policy:allow_attrs("src", "alt", "width", "height"):on_elements("img")
Parameter Type Description
... string Element tag names

Returns: Policy

On All Elements

Allow attributes on every permitted element:

policy:allow_attrs("class"):globally()
policy:allow_attrs("id"):globally()

Returns: Policy

With Pattern Matching

Require attribute values to match a regular expression:

-- Only allow hex colors in style
local builder, err = policy:allow_attrs("style"):matching("^color:#[0-9a-fA-F]{6}$")
if err then
    return nil, err
end
builder:on_elements("span")

policy:sanitize('<span style="color:#ff0000">Red</span>')
-- '<span style="color:#ff0000">Red</span>'

policy:sanitize('<span style="background:red">Bad</span>')
-- '<span>Bad</span>'
Parameter Type Description
pattern string Go RE2-compatible regular expression

Returns: AttrBuilder, error

URL Security

Standard URLs

Enable the standard URL-handling policy. It requires parseable URLs, permits relative URLs plus mailto, http, and https, and adds rel="nofollow" to allowed linking elements:

policy:allow_elements("a")
policy:allow_attrs("href"):on_elements("a")
policy:allow_standard_urls()

Returns: Policy

URL Schemes

Allow specific URL schemes:

policy:allow_url_schemes("https", "mailto")

policy:sanitize('<a href="https://example.com">OK</a>')
-- '<a href="https://example.com">OK</a>'

policy:sanitize('<a href="javascript:alert(1)">XSS</a>')
-- '<a>XSS</a>'
Parameter Type Description
... string Schemes to allow

Returns: Policy

Relative URLs

Configure whether relative URLs are allowed:

policy:allow_relative_urls(true)

policy:sanitize('<a href="/page">Link</a>')
-- '<a href="/page">Link</a>'
Parameter Type Description
allow boolean Allow relative URLs

Returns: Policy

Require Parseable URLs

Reject URLs that fail to parse cleanly. With true, attribute URLs that the HTML sanitizer cannot parse are stripped instead of passed through.

policy:require_parseable_urls(true)
Parameter Type Description
require boolean Require URLs to be parseable

Returns: Policy

Add rel="nofollow" to links:

policy:allow_attrs("href", "rel"):on_elements("a")
policy:allow_url_schemes("https")
policy:require_parseable_urls(true)
policy:require_nofollow_on_links(true)

policy:sanitize('<a href="https://example.com">Link</a>')
-- '<a href="https://example.com" rel="nofollow">Link</a>'
Parameter Type Description
require boolean Add nofollow

Returns: Policy

Add rel="noreferrer" to links:

policy:require_noreferrer_on_links(true)
Parameter Type Description
require boolean Add noreferrer

Returns: Policy

Add target="_blank" to fully qualified URLs:

policy:allow_attrs("href", "target"):on_elements("a")
policy:allow_url_schemes("https")
policy:require_parseable_urls(true)
policy:add_target_blank_to_fully_qualified_links(true)

policy:sanitize('<a href="https://example.com">Link</a>')
-- '<a href="https://example.com" target="_blank" rel="noopener">Link</a>'
Parameter Type Description
add boolean Add target blank

Returns: Policy

When opening untrusted links in a new tab, also enable require_noreferrer_on_links(true) to suppress referrer leakage and mitigate opener access.

Convenience Methods

Allow Images

Permit <img> with align, alt, height, width, and src. This helper also enables the standard URL policy but does not allow data URI images.

policy:allow_images()

policy:sanitize('<img src="photo.jpg" alt="Photo">')
-- '<img src="photo.jpg" alt="Photo">'

Returns: Policy

Allow Data URI Images

Permit syntactically valid Base64-encoded gif, jpeg, png, svg+xml, or webp data URI images. The sanitizer validates the media type and Base64 encoding, not the decoded image contents. Data URIs can carry active content, so enable them only for content whose image data you trust:

policy:allow_elements("img")
policy:allow_attrs("src"):on_elements("img")
policy:allow_data_uri_images()

local input = '<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl2O9sAAAAASUVORK5CYII=">'
policy:sanitize(input)
-- The data URI is preserved.

Returns: Policy

Allow Lists

Permit ul, ol, li, dl, dt, and dd. The helper also allows validated type attributes on ul, ol, and li, plus an integer value attribute on li.

policy:allow_lists()

policy:sanitize('<ul><li>Item 1</li><li>Item 2</li></ul>')
-- '<ul><li>Item 1</li><li>Item 2</li></ul>'

Returns: Policy

Allow Tables

Permit table elements: table, caption, col, colgroup, thead, tbody, tfoot, tr, td, th.

policy:allow_tables()

policy:sanitize('<table><tr><td>Cell</td></tr></table>')
-- '<table><tr><td>Cell</td></tr></table>'

Returns: Policy

Allow Standard Attributes

Permit common attributes: id, title, dir, lang.

policy:allow_elements("p")
policy:allow_standard_attributes()

policy:sanitize('<p id="intro" class="text" title="Introduction">Hello</p>')
-- '<p id="intro" title="Introduction">Hello</p>'

Returns: Policy

Sanitize

Apply a policy to an HTML string:

local policy, err = html.sanitize.ugc_policy()
if err then return nil, err end
policy:require_nofollow_on_links(true)

local dirty = '<p>Hello</p><script>alert("xss")</script>'
local clean = policy:sanitize(dirty)
-- '<p>Hello</p>'
Parameter Type Description
html string HTML to sanitize

Returns: string

sanitize returns only a string. In runtime v0.3.32a, the underlying fragment parser can turn malformed input that it cannot parse into an empty string, and the Lua wrapper cannot distinguish that case from valid input whose content the policy removed. Treat sanitization as output filtering, not input validation; validate required content separately when an empty result matters.

Errors

Condition Kind Retryable
Invalid regex pattern errors.INVALID no

See Error Handling for working with errors.