bleach HTML sanitizer + MarkupSafe Markup: working versions (tested 2026-10-15)
Sanitize untrusted HTML with bleach, then wrap the safe result in MarkupSafe Markup so it embeds in templates without double escaping.
passed6 passed · 0 failed2026-10-15 15:57 UTC- Status
- passed
- Last passed
- 2026-10-15 15:57 UTC
- Tests
- 6 / 6
- Language
- Python
- Runtime
- Python 3.13.16
pinned versions3
bleach@6.4.0markupsafe@3.0.4pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt .venv/bin/python -m pytest -q
test environment
Python 3.13.16 linux 6.17.0-1022-azure x64 runner: github-actions 38764699834
The code
Exactly the files that passed. Your agent can pull them with get_recipe("bleach-markupsafe-html") (install), or see them on GitHub.
requirements.txt
bleach==6.4.0 markupsafe==3.0.4 pytest==9.1.1
sanitizer.py
import bleach
from markupsafe import Markup
ALLOWED_TAGS = ["a", "b", "code", "em", "i", "li", "ol", "p", "strong", "ul"]
ALLOWED_ATTRS = {"a": ["href", "title"]}
ALLOWED_PROTOCOLS = ["http", "https", "mailto"]
def sanitize(html: str) -> str:
# strip=False escapes disallowed tags, strip=True deletes them.
return bleach.clean(
html,
tags=ALLOWED_TAGS,
attributes=ALLOWED_ATTRS,
protocols=ALLOWED_PROTOCOLS,
strip=False,
strip_comments=True,
)
def sanitize_strict(html: str) -> str:
return bleach.clean(html, tags=[], attributes={}, protocols=[], strip=True)
def safe(html: str) -> Markup:
# bleach always returns a plain str, so mark it safe AFTER cleaning.
return Markup(sanitize(html))
def render_card(title: str, body_html: str) -> Markup:
template = Markup('<article class="card"><h1>{}</h1><div>{}</div></article>')
# Markup.format escapes plain args but leaves Markup args alone.
return template.format(title, safe(body_html))
def autolink(text: str) -> Markup:
# linkify alone does NOT sanitize, so clean first to avoid passing scripts through.
return Markup(bleach.linkify(sanitize_strict(text)))
tests/test_sanitizer.py
import bleach
from markupsafe import Markup
from sanitizer import autolink, render_card, sanitize, sanitize_strict
def test_disallowed_tags_are_escaped_by_default():
out = sanitize("<b>hi</b> <script>alert(1)</script>")
assert "<b>hi</b>" in out
assert "<script>alert(1)</script>" in out
assert "<script>" not in out
def test_strip_true_drops_tags_and_comments():
assert sanitize_strict("a<!-- secret --><b>hi</b>") == "ahi"
def test_javascript_urls_and_event_handlers_are_dropped():
out = sanitize('<a href="javascript:alert(1)" onclick="steal()">x</a>')
assert out == "<a>x</a>"
def test_plain_bleach_output_is_double_escaped_by_markup_format():
cleaned = sanitize("<b>hi</b>")
assert isinstance(cleaned, str) and not isinstance(cleaned, Markup)
template = Markup("<div>{}</div>")
assert template.format(cleaned) == "<div><b>hi</b></div>"
assert template.format(Markup(cleaned)) == "<div><b>hi</b></div>"
def test_render_card_escapes_title_but_trusts_sanitized_body():
out = render_card("<script>alert(1)</script>", "hello <em>world</em> <script>x</script>")
assert isinstance(out, Markup)
assert out.startswith('<article class="card"><h1><script>alert(1)</script></h1>')
assert "<em>world</em>" in out
assert "<script>" not in out
def test_linkify_alone_does_not_sanitize_so_clean_first():
assert "<script>" in bleach.linkify("<script>x</script> https://example.com")
out = autolink("<script>x</script> https://example.com")
assert "<script>" not in out
assert '<a href="https://example.com" rel="nofollow">' in out
pytest.ini
[pytest] pythonpath = .
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.