fcf433a736a8670c9da0e2af3ac54db24b0aaa3b braney Mon Aug 24 14:47:47 2026 -0700 htmlSanitize: rename the ids that come in with the HTML, refs #38126 The description HTML is printed inside a page of ours, and it is our own JavaScript that looks ids up. A page that carries an id we already use puts two elements of that name in one document, and getElementById returns whichever comes first. Two ids in the public hub pages collide with ours today, one of them "content". An id also becomes a property of that name on window, which reaches the guards we write as "typeof X !== 'undefined' && X". Every id, and every name on an anchor, now gets the prefix descPage- . The hyphen means the window property it makes can never be spelled as a JavaScript name, so it cannot stand in for one of our globals either. A link to a name on the same page, href="#x", is rewritten with the same prefix and keeps working. A link that names another page is left alone: it leaves our page for the hub's own file, where the names are unchanged. Measured over the 5390 description pages of the public hubs: 1643 ids and anchor names renamed, no id left without the prefix, and the number of same page links with nothing to point at is 22 before and 22 after, the same ones. No page loses reader-visible text and the hubCheck warnings are unchanged at 118 pages, since a reader sees nothing of this. diff --git src/lib/htmlSanitize.c src/lib/htmlSanitize.c index cf5f1187685..5281b97a5c9 100644 --- src/lib/htmlSanitize.c +++ src/lib/htmlSanitize.c @@ -1,24 +1,28 @@ /* htmlSanitize - reduce a piece of HTML that came from outside to an allowlist of * elements, attributes and style properties. * * An element on the keep list survives with its allowlisted attributes. A short list of * elements that carry nothing a reader needs, script and style and form among them, is * removed together with everything inside. Every other element loses its tag but keeps * its text, which is what lets a whole pasted document come out as the article it was * meant to be. * + * Every id, and every name on an anchor, is renamed with a fixed prefix. The result is + * printed inside a page of ours, so a name the outside HTML chose could otherwise be one + * our own JavaScript looks up. A link to a name on the same page is rewritten to match. + * * The parser here is deliberately forgiving. It never aborts and it always returns * something, however broken the markup it is handed. */ /* Copyright (C) 2026 The Regents of the University of California * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */ #include "common.h" #include "hash.h" #include "dystring.h" #include "htmlSanitize.h" /* Elements we keep. */ static char *keepElements = "p br hr div span center address blockquote pre " "h1 h2 h3 h4 h5 h6 " @@ -605,31 +609,57 @@ if (sameString(attr, "style")) { char *style = filterStyle(value, san); if (style != NULL) { dyStringAppend(san->out, " style=\""); appendEscaped(san->out, style); dyStringAppendC(san->out, '"'); freeMem(style); } } else if (sameString(attr, "href") || sameString(attr, "src")) { if (urlOk(value, san)) { + char *fragment = NULL; + if (sameString(attr, "href")) + { + char *trimmed = skipLeadingSpaces(value); + if (trimmed[0] == '#' && trimmed[1] != 0) + fragment = trimmed + 1; /* a link to a name on this same page */ + } dyStringPrintf(san->out, " %s=\"", attr); + if (fragment != NULL) + { + /* The name it points at is being renamed, so rename this to match. */ + dyStringAppend(san->out, "#" htmlSanitizeIdPrefix); + appendEscaped(san->out, fragment); + } + else + appendEscaped(san->out, value); + dyStringAppendC(san->out, '"'); + } + } + else if (sameString(attr, "id") || (isAnchor && sameString(attr, "name"))) + { + /* An id here lands in a page of ours, next to ids our own JavaScript looks up, + * and it also becomes a property of that name on window. A prefix keeps the two + * sets apart. An anchor name does both of those things too. */ + if (isNotEmpty(value)) + { + dyStringPrintf(san->out, " %s=\"%s", attr, htmlSanitizeIdPrefix); appendEscaped(san->out, value); dyStringAppendC(san->out, '"'); } } else if (isAnchor && sameString(attr, "rel")) { freez(&relValue); relValue = cloneString(value); } else { if (isAnchor && sameString(attr, "target")) hasTarget = TRUE; dyStringPrintf(san->out, " %s=\"", attr); appendEscaped(san->out, value);