import html from html.parser import HTMLParser _ALLOWED_TAGS = { "b", "strong", "i", "em", "u", "p", "br", "ul", "ol", "li", "blockquote", "code", "pre", "a", "h1", "h2", "h3", "h4", "h5", "h6", } _ALLOWED_ATTRS = { "a": {"href", "title", "target", "rel"}, } _ALLOWED_SCHEMES = ("http://", "https://", "mailto:") def _escape_attr(value: str) -> str: return html.escape(value, quote=True) def sanitize_html_fragment(raw: str) -> str: class SafeHtmlParser(HTMLParser): def __init__(self): super().__init__(convert_charrefs=True) self.parts = [] def handle_starttag(self, tag, attrs): tag = tag.lower() if tag not in _ALLOWED_TAGS: return allowed = _ALLOWED_ATTRS.get(tag, set()) clean_attrs = [] for key, value in attrs: key = (key or "").lower() value = value or "" if key not in allowed: continue if tag == "a" and key == "href": lowered = value.strip().lower() if not lowered.startswith(_ALLOWED_SCHEMES): continue clean_attrs.append((key, value)) if tag == "a": attr_keys = {key for key, _ in clean_attrs} if "target" not in attr_keys: clean_attrs.append(("target", "_blank")) if "rel" not in attr_keys: clean_attrs.append(("rel", "noopener noreferrer")) attr_text = "".join( f' {key}="{_escape_attr(value)}"' for key, value in clean_attrs ) self.parts.append(f"<{tag}{attr_text}>") def handle_endtag(self, tag): tag = tag.lower() if tag in _ALLOWED_TAGS and tag not in {"br"}: self.parts.append(f"") def handle_data(self, data): self.parts.append(html.escape(data)) def handle_entityref(self, name): self.parts.append(f"&{name};") def handle_charref(self, name): self.parts.append(f"&#{name};") parser = SafeHtmlParser() parser.feed(raw or "") parser.close() return "".join(parser.parts)