f83fd4fd350727eb3a42a709849bab0c4658ab84
braney
  Fri Aug 21 10:17:16 2026 -0700
htmlSanitize: three fixes from a review of it, refs #38126

Read the scheme of an href or src the way a browser arrives at it.  A browser
turns a numeric character reference into its character before it decides what
the scheme is, and it accepts one written with any number of leading zeros and
with no closing semicolon at all.  Decode those the same way, then insist that
what stands in front of the first slash is either a plain scheme we allow or a
plain path.  A named entity, a backslash or a control character in that part of
the value means we do not print the link, because those are the ways the check
gets walked around.  This keeps the 33 encoded mailto links two hubs write, and
they were the only links in the public hubs the plainer rule would have lost.

Treat a trailing slash on a kept element as the nothing that HTML says it is.
Otherwise <div/> came out as an open div and the rest of our own page sat
inside it.

Remember when the search for a closing tag has run off the end of the input.
Every later search for that same tag runs off the end too, so a page made of
two hundred thousand unclosed tags no longer costs one pass over the page each.

The unit test grows a case for each of the three.

diff --git src/lib/tests/htmlSanitizeTest.c src/lib/tests/htmlSanitizeTest.c
index 7f9f10b1605..df0f96b8c48 100644
--- src/lib/tests/htmlSanitizeTest.c
+++ src/lib/tests/htmlSanitizeTest.c
@@ -4,45 +4,52 @@
  * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */
 
 #include "common.h"
 #include "htmlSanitize.h"
 
 static char *cases[] = {
 /* a whole pasted document comes out as the article it was meant to be */
 "<html><head><title>T</title><meta charset=\"utf-8\"></head><body class=\"x\">"
     "<h2>Head</h2><p>Text</p></body></html>",
 /* script and style go, with their contents */
 "<p>before</p><script>alert(1)</script><style>body{visibility:hidden}</style><p>after</p>",
 /* a script that is never closed takes the rest with it */
 "<p>before</p><script>if (a < b) alert(1)",
 /* event handlers and class go, id and title stay */
 "<div id=\"top\" class=\"warn\" title=\"t\" onclick=\"alert(1)\">text</div>",
-/* an entity encoded scheme is still that scheme */
+/* an entity encoded scheme is still that scheme, however it is spelled */
 "<a href=\"&#106;avascript:alert(1)\">one</a> <a href=\"java&Tab;script:alert(1)\">two</a>",
+"<a href=\"&#0000000106;avascript:alert(1)\">three</a> <a href=\"&#106avascript:alert(1)\">four</a>",
+"<a href=\"jav&#9;ascript:alert(1)\">five</a> <a href=\"java&colon;script:alert(1)\">six</a>",
+/* an address that only looks encoded is left working */
+"<a href=\"&#109;ailto:help&#64;riken.jp?subject=x\">write to us</a>",
 /* ordinary links are left alone, and a new window does not get a handle on ours */
 "<a href=\"https://genome.ucsc.edu\">u</a> <a href=\"#anchor\" target=\"_blank\">a</a>",
 /* an image keeps its source, not its onerror */
 "<img src=\"pic.png\" alt=\"a\" onerror=\"alert(1)\" width=\"20\">",
 /* a frame survives only when it plays a video from a host we know */
 "<iframe width=\"560\" src=\"https://www.youtube.com/embed/abc\"></iframe>"
     "<iframe src=\"https://example.com/x\">fallback</iframe>",
 /* the style attribute is filtered a property at a time */
 "<p style=\"color:red;behavior:url(#default#VML);text-align:center;position:fixed\">p</p>",
 /* unknown elements lose their tag and keep their text */
 "<o:p>word</o:p><vertebrates>more</vertebrates>",
-/* tags left open are closed for us */
+/* tags left open are closed for us, and a slash does not close one of these */
 "<div><b>bold<p>para",
+"<div style=\"color:red\"/>the rest of the page is not inside that div",
+/* a page built of tags that are never closed is not a way to make us work all day */
+"<svg><svg><svg><svg>text",
 /* a stray quote inside an unquoted value does not swallow the page */
 "<a href=https://example.com/x\\\">link text</a> and more text",
 /* a form and everything in it goes */
 "<form action=\"/x\"><input name=\"password\"><button>Log in</button></form><p>after</p>",
 /* comments and doctypes go */
 "<!DOCTYPE html><!-- <p>hidden</p> --><p>shown</p>",
 /* a table keeps its shape */
 "<table border=\"1\"><tr><td colspan=\"2\" bgcolor=\"#eee\">cell</td></tr></table>",
 };
 
 int main(int argc, char *argv[])
 {
 int i;
 for (i = 0;  i < ArraySize(cases);  ++i)
     {