JustHTML’s sanitization and transform pipeline were heavily inspired by Bleach’s real-world ergonomics.
Bleach has helped a lot of projects ship safer HTML over the years, and a lot of that is thanks to the hard work of @willkg building and maintaining it.
In 2023, Bleach’s maintainer announced that Bleach is deprecated (but will continue to receive security updates, new Python version support, and fixes for egregious bugs). See: https://github.com/mozilla/bleach/issues/698
This guide covers common migration patterns.
A large application migration often works best in two steps:
clean(...) wrapper that accepts the Bleach-shaped options your codebase already uses.bleach.clean(...) / bleach.linkify(...) to that wrapper.That keeps most call-site changes mechanical while leaving the security policy in one place.
Example wrapper:
import re
from collections.abc import Collection, Mapping
from justhtml import JustHTML, Linkify, SanitizationPolicy, SetAttrs, UrlPolicy, UrlRule
URL_LIKE_ATTRS = {
"href",
"src",
"srcset",
"poster",
"action",
"formaction",
"data",
"cite",
"background",
"ping",
}
def build_url_policy(
allowed_tags: Collection[str],
allowed_attributes: Mapping[str, Collection[str]],
) -> UrlPolicy:
rules = {}
global_attrs = allowed_attributes.get("*", ())
for tag in allowed_tags:
for attr in global_attrs:
if attr in URL_LIKE_ATTRS:
rules[(tag, attr)] = UrlRule(
allowed_schemes={"http", "https", "mailto", "tel"},
allow_relative=True,
)
for tag, attrs in allowed_attributes.items():
if tag == "*":
continue
for attr in attrs:
if attr in URL_LIKE_ATTRS:
rules[(tag, attr)] = UrlRule(
allowed_schemes={"http", "https", "mailto", "tel"},
allow_relative=True,
)
return UrlPolicy(allow_rules=rules)
def clean(
html: str,
*,
tags: Collection[str] = (),
attributes: Mapping[str, Collection[str]] | None = None,
css_properties: Collection[str] = (),
strip: bool = True,
strip_comments: bool = True,
) -> str:
attrs = dict(attributes or {})
disallowed_tag_handling = "unwrap" if strip else "escape"
# Bleach defaults to strip_comments=True. If you use escape mode
# (`strip=False`) and still want comments removed rather than displayed,
# remove comments before parsing.
if strip_comments and disallowed_tag_handling == "escape":
html = re.sub(r"<!--.*?-->", "", html, flags=re.DOTALL)
policy = SanitizationPolicy(
allowed_tags=tags,
allowed_attributes=attrs,
allowed_css_properties=css_properties,
url_policy=build_url_policy(tags, attrs),
drop_comments=strip_comments,
disallowed_tag_handling=disallowed_tag_handling,
)
return JustHTML(html, fragment=True, policy=policy).to_html(pretty=False)
def linkify(text: str, *, nofollow: bool = False) -> str:
transforms = [Linkify()]
if nofollow:
transforms.append(SetAttrs("a", rel="nofollow"))
return JustHTML(
text,
fragment=True,
sanitize=False,
transforms=transforms,
).to_html(pretty=False)
Treat this as a compatibility shim, not a universal policy. Tighten build_url_policy(...) for your application, especially for attributes that load remote resources such as img[src].
JustHTML(html) sanitizes by default (sanitize=True).JustHTML(html, sanitize=False) disables sanitization (trusted input only).JustHTML also supports constructor-time transforms (a DOM equivalent of Bleach/html5lib filter pipelines): see Transforms.
bleach.clean(...)A typical Bleach call:
import bleach
clean = bleach.clean(
user_html,
tags=["p", "b", "a"],
attributes={"a": ["href"]},
protocols=["http", "https"],
strip=True,
)
In JustHTML you typically configure a SanitizationPolicy:
from justhtml import JustHTML, SanitizationPolicy, UrlPolicy, UrlRule
policy = SanitizationPolicy(
allowed_tags=["p", "b", "a"],
allowed_attributes={"*": [], "a": ["href"]},
url_policy=UrlPolicy(
default_handling="allow",
allow_rules={
("a", "href"): UrlRule(allowed_schemes=["http", "https"]),
},
),
)
doc = JustHTML(user_html, fragment=True, policy=policy)
clean = doc.to_html()
Notes:
fragment=True for user-generated snippets. That avoids adding <html>, <head>, and <body> tags.Sanitize(...) at the end of your transform pipeline (see HTML Cleaning).Bleach supports html5lib filters and helper utilities (like linkifying text).
In JustHTML, you compose transforms (applied once, right after parsing):
bleach.linkify(...) → Linkify(...) (see Linkify)html5lib.filters.whitespace.Filter → CollapseWhitespace(...)Unwrap(selector)Drop(selector)Empty(selector)SetAttrs(selector, **attrs)Edit(selector, func)Example: linkify text, then add safe link attributes:
from justhtml import JustHTML, Linkify, SetAttrs
doc = JustHTML(
"<p>See example.com</p>",
fragment=True,
transforms=[
Linkify(),
SetAttrs("a", rel="nofollow noopener", target="_blank"),
],
)
# Still sanitized by default (construction time)
print(doc.to_html(pretty=False))
Bleach’s protocols=[...] concept maps to JustHTML’s URL policy rules.
UrlRule(allowed_schemes=[...]).(tag, attr) allow rules even when the attribute itself is allowed.sanitize=True, URLs can be rewritten or stripped according to policy (see URL Cleaning).Bleach’s strip option controls whether disallowed tags are removed entirely or escaped.
JustHTML’s sanitizer is allowlist-based and focuses on producing safe markup. Disallowed tags are handled by SanitizationPolicy(disallowed_tag_handling=...), and dangerous containers (like script/style) drop their contents.
Mapping:
strip=True → disallowed_tag_handling="unwrap" (default)
strip=False → disallowed_tag_handling="escape"
JustHTML also supports disallowed_tag_handling="drop" to drop the entire disallowed subtree.
If you need to display untrusted HTML with no HTML in the output, prefer to_text(), or escape the output before embedding it into an HTML page. to_markdown() runs on the sanitized DOM when sanitize=True (the default), but the value it returns is still Markdown source, not escaped HTML. Render it with a compliant Markdown renderer before embedding it into a page, or escape it first if you want to show the raw Markdown source. It may still include sanitized raw HTML for elements such as tables and images.
If you need additional structural cleanup beyond policy decisions, prefer doing it explicitly with transforms.
Expect some output differences even when the security behavior is equivalent:
vertical-align:top; can serialize as vertical-align: top.strip=False maps to escaped disallowed tags. If your old Bleach call also relied on strip_comments=True, add an explicit test for comments.bleach.linkify(...) and bleach.clean(...).bleach.clean(...), bleach.Cleaner(...), and bleach.linkify(...) call shapes.JustHTML(...).to_html()) for new code.SanitizationPolicy that matches your allowlist and URL requirements.