#########################
Sanitize untrusted HTML
#########################
Clean untrusted HTML against an allowlist with :func:`turbohtml.clean.sanitize`, the ``bleach.clean`` successor, keeping
only a safe subset of tags, attributes, and URL schemes.
*************************
Sanitize untrusted HTML
*************************
To clean user-submitted HTML the way ``bleach.clean`` did, use :func:`turbohtml.clean.sanitize`. A
:class:`~turbohtml.clean.Policy` says what to keep (here the ``relaxed`` preset for typical user content), and a
non-overridable baseline drops scripting and ``javascript:`` URLs no matter what the policy allows:
.. testcode::
from turbohtml.clean import sanitize, Policy
print(sanitize("
<script>evil()</script>
*************************************
Keep the output safe for a template
*************************************
When sanitized HTML is later rendered through a client-side template engine (Angular, Vue, Mustache, EJS, ERB), a
surviving ``{{ ... }}``, ``${ ... }``, or ``<% ... %>`` is a second injection point: the engine evaluates it after the
sanitizer has passed. Set ``strip_template_markers`` to collapse every such run, in kept text and attribute values, to a
single space, matching DOMPurify's ``SAFE_FOR_TEMPLATES``.
.. code-block:: python
from turbohtml.clean import sanitize, Policy
policy = Policy.relaxed()
policy = Policy(tags=policy.tags, attributes=policy.attributes, strip_template_markers=True)
print(sanitize("
Hi {{ user.name }}
", policy))
#
Hi
***************************
Neutralize DOM clobbering
***************************
When you allow ``id`` or ``name`` on kept elements, an attacker can set a value that collides with a built-in
``document`` or form property and shadow it through named access -- ```` makes
``form.attributes`` resolve to the input, ```` hides ``document.body``. The allowlist keeps the
attribute because it is otherwise ordinary. Set ``isolate_named_props`` to prefix every kept ``id`` and ``name`` value
with ``user-content-``, moving it out of the property namespace so no value can collide, matching DOMPurify's
``SANITIZE_NAMED_PROPS``. An already-prefixed value is left alone, so re-sanitizing is a fixpoint.
.. testcode::
from turbohtml.clean import sanitize, Policy
policy = Policy(
tags=frozenset({"a", "input"}),
attributes={"a": frozenset({"id", "href"}), "input": frozenset({"name"})},
isolate_named_props=True,
)
print(sanitize('x', policy))
.. testoutput::
x
*********************************************
Restrict inline styles to known-good values
*********************************************
``css_properties`` allowlists property *names*; ``allowed_styles`` narrows further by *value*, the way sanitize-html's
``allowedStyles`` does. Key it ``{tag: {property: [pattern, ...]}}``, with ``"*"`` as a tag matching every element. A
declaration survives only when its property is listed for the element's tag or ``"*"`` and its value matches one of the
patterns (an unanchored :func:`re.search`). This runs on top of ``css_properties`` and the dangerous-value baseline: a
property must still be in ``css_properties``, and ``expression()`` or a ``url()`` with a disallowed scheme is dropped
even if a pattern would admit it.
.. testcode::
from turbohtml.clean import sanitize, Policy
policy = Policy(
tags=frozenset({"p"}),
attributes={"p": frozenset({"style"})},
css_properties=frozenset({"color", "text-align"}),
allowed_styles={"*": {"color": [r"^#[0-9a-f]{3,6}$"], "text-align": [r"^left$|^center$|^right$"]}},
)
print(sanitize('
Hi
', policy))
.. testoutput::
Hi
******************************
Rename tags while sanitizing
******************************
To rewrite a tag as it is cleaned -- ```` to ````, or the deprecated ``
`` to a ``
`` -- set
``transform_tags``, sanitize-html's ``transformTags``. Key it by source tag: a bare string renames, and a
:class:`~turbohtml.clean.Transform` renames and adds attributes. The rename runs before the allowlist, so the renamed
element is re-checked from scratch; a transform sets an element's name, never its safety. Mapping a tag to a disallowed
or unsafe target (``script``) still drops it, and an added attribute is scrubbed with the element's own, so it must be
allowlisted to survive.
.. testcode::
from turbohtml.clean import sanitize, Policy, Transform
policy = Policy(
tags=frozenset({"strong", "em", "div"}),
attributes={"div": frozenset({"class"})},
transform_tags={"b": "strong", "i": "em", "center": Transform("div", {"class": "legacy"})},
)
print(sanitize("
bold and italic
", policy))
.. testoutput::
bold and italic
************************************
Allow an app's own custom elements
************************************
To keep your own custom elements (````, ````) without enumerating every one in ``tags``, give the
policy a matcher, DOMPurify's ``CUSTOM_ELEMENT_HANDLING``. ``custom_element_check`` receives an unlisted element's
lowercased tag name and returns true to keep it; ``custom_attribute_check`` receives ``(tag, attribute_name)`` and
decides which of a kept custom element's attributes survive. Only basic custom-element names (hyphenated, not a reserved
name like ``annotation-xml``) reach the matcher, and the safety baseline still runs on whatever it keeps -- ``on*``
handlers, ``javascript:`` URLs, and dangerous styles are dropped regardless.
.. testcode::
from turbohtml.clean import sanitize, Policy
policy = Policy(
tags=frozenset({"p"}),
custom_element_check=lambda tag: tag.startswith("x-"),
custom_attribute_check=lambda _tag, name: name.startswith("data-"),
)
print(sanitize('
starsno
', policy))
.. testoutput::
stars<y-ad>no</y-ad>
Pass ``re.compile(r"^x-").search`` as the matcher to drive it from a regular expression, and set
``allow_customized_builtins`` to also keep an ``is`` attribute whose value names a custom element (``