4037b582757f86eed1c5559eca9ccc7af85c1496
braney
  Fri Aug 21 10:00:49 2026 -0700
lib: add htmlSanitize, an allowlist filter for HTML written elsewhere, refs #38126

htmlSanitize() takes a piece of HTML and returns a copy holding only the
elements, attributes and style properties on its lists.  An element on the
keep list survives with its allowed attributes.  A short list of elements
that carry nothing for a reader, script and style and form among them, is
dropped along with its contents.  Every other element loses its tag and
keeps its text, so a whole document that somebody saved and pasted in comes
out as the article it was meant to be.

The lists come from a survey of all 5390 description pages reachable from
the public hub list, so they are sized to what hubs actually write.  The
style attribute is filtered a property at a time, and href and src are
checked for a scheme we do not print, after decoding entities and padding.
An iframe is kept only when it plays a video from one of a few hosts, and
then with a sandbox attribute.

htmlSanitizeReport() returns the same copy plus a list of one-line messages
naming what came out, for hubCheck to show a hub author.

The tokenizer is hand written and forgiving.  It never aborts and always
returns something, because the HTML it will be handed is often broken.
lib/htmlPage.c cannot be reused for this: its parser aborts on bad input.

diff --git src/lib/tests/expected/htmlSanitizeTest src/lib/tests/expected/htmlSanitizeTest
new file mode 100644
index 00000000000..90d323314cd
--- /dev/null
+++ src/lib/tests/expected/htmlSanitizeTest
@@ -0,0 +1,53 @@
+in : <html><head><title>T</title><meta charset="utf-8"></head><body class="x"><h2>Head</h2><p>Text</p></body></html>
+out: <h2>Head</h2><p>Text</p>
+
+in : <p>before</p><script>alert(1)</script><style>body{visibility:hidden}</style><p>after</p>
+out: <p>before</p><p>after</p>
+     (removed the script element and everything inside it)
+     (removed the style element and everything inside it)
+
+in : <p>before</p><script>if (a < b) alert(1)
+out: <p>before</p>
+     (removed the script element and everything inside it)
+
+in : <div id="top" class="warn" title="t" onclick="alert(1)">text</div>
+out: <div id="top" title="t">text</div>
+     (removed the attribute onclick)
+
+in : <a href="&#106;avascript:alert(1)">one</a> <a href="java&Tab;script:alert(1)">two</a>
+out: <a>one</a> <a>two</a>
+     (removed a link that used the javascript: scheme)
+
+in : <a href="https://genome.ucsc.edu">u</a> <a href="#anchor" target="_blank">a</a>
+out: <a href="https://genome.ucsc.edu">u</a> <a href="#anchor" target="_blank" rel="noopener noreferrer">a</a>
+
+in : <img src="pic.png" alt="a" onerror="alert(1)" width="20">
+out: <img src="pic.png" alt="a" width="20">
+     (removed the attribute onerror)
+
+in : <iframe width="560" src="https://www.youtube.com/embed/abc"></iframe><iframe src="https://example.com/x">fallback</iframe>
+out: <iframe width="560" src="https://www.youtube.com/embed/abc" sandbox="allow-scripts allow-same-origin allow-popups allow-presentation"></iframe>
+     (removed an iframe, we only allow one that plays a video from a site we know)
+
+in : <p style="color:red;behavior:url(#default#VML);text-align:center;position:fixed">p</p>
+out: <p style="color:red;text-align:center;">p</p>
+
+in : <o:p>word</o:p><vertebrates>more</vertebrates>
+out: wordmore
+
+in : <div><b>bold<p>para
+out: <div><b>bold<p>para</p></b></div>
+
+in : <a href=https://example.com/x\">link text</a> and more text
+out: <a href="https://example.com/x\&quot;">link text</a> and more text
+
+in : <form action="/x"><input name="password"><button>Log in</button></form><p>after</p>
+out: <p>after</p>
+     (removed the form element and everything inside it)
+
+in : <!DOCTYPE html><!-- <p>hidden</p> --><p>shown</p>
+out: <p>shown</p>
+
+in : <table border="1"><tr><td colspan="2" bgcolor="#eee">cell</td></tr></table>
+out: <table border="1"><tr><td colspan="2" bgcolor="#eee">cell</td></tr></table>
+