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/htmlSanitizeTest.c src/lib/tests/htmlSanitizeTest.c new file mode 100644 index 00000000000..7f9f10b1605 --- /dev/null +++ src/lib/tests/htmlSanitizeTest.c @@ -0,0 +1,63 @@ +/* htmlSanitizeTest - check that htmlSanitize keeps what it should and drops the rest. */ + +/* 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 "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 */ +"<a href=\"javascript:alert(1)\">one</a> <a href=\"java	script:alert(1)\">two</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 */ +"<div><b>bold<p>para", +/* 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) + { + char *clean = htmlSanitize(cases[i]); + printf("in : %s\nout: %s\n", cases[i], clean); + struct slName *removed = NULL, *el; + freeMem(clean); + clean = htmlSanitizeReport(cases[i], &removed); + for (el = removed; el != NULL; el = el->next) + printf(" (%s)\n", el->name); + printf("\n"); + freeMem(clean); + slFreeList(&removed); + } +if (htmlSanitize(NULL) != NULL) + errAbort("htmlSanitize(NULL) should be NULL"); +return 0; +}