Sanitize HTML with a tag allowlist
Your task
Comments on a forum may contain a little HTML. Write the sanitizer that runs before a comment is stored: it keeps a small allowlist of harmless markup and turns everything else into inert text or removes it. Scan html once, left to right.
Tokens. <!-- starts a comment, which runs to the next --> after it (or to the end of the input) and is removed. </ followed by an ASCII letter starts an end tag, which runs to the next >. < followed by an ASCII letter starts a start tag, which runs to the next > that is not inside a quoted attribute value. A tag name is the run of ASCII letters, digits and hyphens after the < or </, compared without regard to ASCII case. If a tag never reaches its >, its < is an ordinary character and the scan continues right after it; any other < is an ordinary character too. Everything between tokens is text.
Attributes (start tags only). After the tag name, whitespace (space, tab, newline, carriage return, form feed), / and stray = are skipped. An attribute is a name, a run of characters other than whitespace, /, > and =, compared without regard to ASCII case, optionally followed by = and a value, with optional whitespace around the =. A value is double-quoted, single-quoted, or unquoted (a run of characters other than whitespace and >); a missing value is the empty string. If a name repeats, its first occurrence wins.
Character references. Text and attribute values are decoded once, replacing exactly these: & < > " ', decimal A and hexadecimal A (with x or X). The ; is required, and a numeric reference whose code point is 0, above 10FFFF (hex) or between D800 and DFFF stays as it is, like any other &. Each run of text between two tokens is decoded on its own.
Elements.
scriptandstyleare removed with everything inside them. Nothing inside is parsed: the content ends at the first</scriptor</style(matching the start tag, any case) that is followed by whitespace,/or>, and that end tag runs to its next>. With no such end tag, everything to the end of the input is removed.p,br,b,i,em,strong,code,ul,ol,liandaare allowed. A start tag is written as<name>in lowercase, with every attribute dropped excepthrefandtitleon ana, which keep their source order.bris written as<br>, never opens anything, and an end tag for it is dropped.- Any other element loses its tags, but its content is processed as usual.
Nesting. Keep a stack of open allowed elements. An end tag for an allowed element that is open writes the end tags of every element opened after it, innermost first, then its own, and pops them all. An end tag for an element that is not open is dropped. At the end of the input, everything still open is closed, innermost first.
Links. For an href, take the decoded value, delete every tab, newline and carriage return in it, and strip characters U+0000 to U+0020 from both ends. If the cleaned value contains a : and no /, ? or # comes before the first one, the text before that :, lowercased, is the scheme, and the href is kept only if the scheme is http, https or mailto. With no scheme it is a relative URL and is kept. A kept href is written with its cleaned value; a rejected one is dropped, and the a stays.
Output. Text is written with &, < and > escaped as &, < and >. Attribute values are written in double quotes with &, ", < and > escaped as &, ", < and >.
Examples:
sanitize_html('<p onclick="steal()">Hi <b>there</b><script>alert(1)</script></p>')returns'<p>Hi <b>there</b></p>'sanitize_html('<a href="JaVaScRiPt:alert(1)" title=x>click</a> <a href="/docs?a=1&b=2">docs</a>')returns'<a title="x">click</a> <a href="/docs?a=1&b=2">docs</a>'sanitize_html('<div><i>unclosed <em>tags</i> 5 < 6</div>')returns'<i>unclosed <em>tags</em></i> 5 < 6'
Examples
sanitize_html(html: str) -> strsanitize_html("<p onclick=\"steal()\">Hi <b>there</b><script>alert(1)</script></p>")"<p>Hi <b>there</b></p>"sanitize_html("<a href=\"JaVaScRiPt:alert(1)\" title=x>click</a> <a href=\"/docs?a=1&b=2\">docs</a>")"<a title=\"x\">click</a> <a href=\"/docs?a=1&b=2\">docs</a>"sanitize_html("<div><i>unclosed <em>tags</i> 5 < 6</div>")"<i>unclosed <em>tags</em></i> 5 < 6"