← Back to docs

HTML Cleaning

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:

Safe-by-default construction

By default, construction removes all dangerous html:

HTML output

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&amp;b=2">ok</a></p>

Markdown output

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)

Default sanitization policy

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:

Disallowed tags

Disallowed tag handling is controlled by SanitizationPolicy(disallowed_tag_handling=...):

Default allowlists:

Use a custom sanitization policy

Start 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:

For URL schemes, hosts, proxies, srcset, and other URL-specific controls, continue with URL Cleaning.

Sanitizing the in-memory DOM

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 (optional)

Inline styles are disabled by default. To allow them you must:

  1. Allow the style attribute for the relevant tag via allowed_attributes.
  2. Provide a non-empty allowlist via 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())

Advanced policy options

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.

Disable sanitization

Only disable sanitization for HTML you fully trust:

doc = JustHTML(trusted_html, fragment=True, sanitize=False)

Reporting issues

If you find a sanitizer bypass, please report it responsibly (see SECURITY.md).