JustHTML includes a built-in, policy-driven HTML sanitizer intended for rendering untrusted HTML safely.
This page focuses on HTML cleaning: tags, attributes, and inline styles. For URL validation and rewriting, see URL Cleaning.
On this page:
By default, construction removes all dangerous html:
from justhtml import JustHTML
user_html = '<p>Hello <b>world</b> <script>alert(1)</script> <a href="javascript:alert(1)">bad</a> <a href="https://example.com/?a=1&b=2">ok</a></p>'
doc = JustHTML(user_html, fragment=True)
print(doc.to_html())
<p>Hello <b>world</b> <a>bad</a> <a href="https://example.com/?a=1&b=2">ok</a></p>
from justhtml import JustHTML
user_html = '<p>Hello <b>world</b> <script>alert(1)</script> <a href="javascript:alert(1)">bad</a> <a href="https://example.com/?a=1&b=2">ok</a></p>'
doc = JustHTML(user_html, fragment=True)
print(doc.to_markdown())
Hello **world** [bad] [ok](https://example.com/?a=1&b=2)
The built-in default is DEFAULT_POLICY (a conservative allowlist).
The default URL policy is conservative about remote loads: by default a[href] allows common link schemes, while img[src] only allows relative URLs (so images won’t load from remote hosts unless you opt in via a custom policy). For details, see URL Cleaning.
High-level behavior:
script/style have their content dropped.sanitize=False.on*), srcdoc, and namespace-style attributes (anything with :) are removed.Disallowed tag handling is controlled by SanitizationPolicy(disallowed_tag_handling=...):
"unwrap" (default): remove the disallowed tag, keep/sanitize its children"escape": emit the disallowed tag’s start/end tags as escaped text, keep/sanitize its children"drop": drop the entire disallowed subtreeDefault allowlists:
a, img, common text/structure tags, headings, lists, and tables (table, thead, tbody, tfoot, tr, th, td).class, id, title, lang, dira: href, titleimg: src, alt, title, width, height, loading, decodingth/td: colspan, rowspanStart with the smallest policy that matches the HTML you want to accept. This makes the allowed output clear to future maintainers.
from justhtml import JustHTML, SanitizationPolicy, UrlPolicy, UrlRule
policy = SanitizationPolicy(
allowed_tags={"p", "strong", "a"},
allowed_attributes={"a": {"href"}},
url_policy=UrlPolicy(
allow_rules={
("a", "href"): UrlRule(allowed_schemes={"https", "mailto"}),
}
),
)
safe_html = JustHTML(user_html, fragment=True, policy=policy).to_html()
The three settings work together:
allowed_tags controls which elements remain.allowed_attributes controls which attribute names remain. An omitted tag has no allowed attributes, so you only need to list tags that accept attributes.href also need a UrlRule; allowing the attribute name alone is not enough.For URL schemes, hosts, proxies, srcset, and other URL-specific controls, continue with URL Cleaning.
The parsed DOM is sanitized by default at construction time (JustHTML(..., sanitize=True)), and serialization is a pure output step.
If you want to sanitize after other transforms or after direct DOM edits, add Sanitize(...) at the point where the tree should become safe. Later transforms can reintroduce unsafe content. For explicit pass boundaries (advanced use), see Stage([...]).
from justhtml import JustHTML, Sanitize
doc = JustHTML(user_html, fragment=True, transforms=[Sanitize()])
print(doc.to_html(pretty=False))
Inline styles are disabled by default. To allow them you must:
style attribute for the relevant tag via allowed_attributes.allowed_css_properties.Even then, JustHTML rejects declarations that look like they can load external resources (such as values containing url( or image-set(), as well as legacy constructs like expression(. Start from the conservative CSS_PRESET_TEXT preset.
from justhtml import CSS_PRESET_TEXT, JustHTML, SanitizationPolicy, UrlPolicy
policy = SanitizationPolicy(
allowed_tags={"p"},
allowed_attributes={"p": {"style"}},
url_policy=UrlPolicy(allow_rules={}),
allowed_css_properties=CSS_PRESET_TEXT | {"width"},
)
html = '<p style="color: red; background-image: url(https://evil.test/x); width: expression(alert(1));">Hi</p>'
print(JustHTML(html, policy=policy).to_html())
Selector limits are for trusted pipelines that need to accept unusually large selectors or match budgets:
from justhtml import SanitizationPolicy
from justhtml.selector import SelectorLimits
policy = SanitizationPolicy(
allowed_tags={"div", "p"},
allowed_attributes={"*": {"class"}},
selector_limits=SelectorLimits(max_length=20_000),
)
Treat policies that allow active content as a separate security review: iframe, object, embed, meta, link, base, form elements, and their active attributes are preserved when you explicitly allow them.
Only disable sanitization for HTML you fully trust:
doc = JustHTML(trusted_html, fragment=True, sanitize=False)
If you find a sanitizer bypass, please report it responsibly (see SECURITY.md).