e6ddf60465deb96e43be6738c5ca6a7a6168cac8 braney Sat Aug 22 15:08:59 2026 -0700 hg/lib: add an option to send the content policy as an http response header New cspWriteResponseHeader() in hg/lib/hCommon.c, gated on hg.conf's cspResponseHeader, which defaults off everywhere. The policy string itself is still built by the existing code in lib/htmshell.c, which now also knows how to format it as a response header. Both carry the same nonce, since getNonce() is one per process, so a page may safely have the header and the meta tag. Most pages pick it up from addHttpHeaders() in cart.c, the existing hook for extra response headers, which every cart based CGI already passes through. Six places build their own http header block and so call it directly: the two "too many requests" pages, the captcha and its error page, the hubApi help redirect, and the hgSearch redirect to hgTracks. Inline scripts on three of those pages now carry the nonce, and the policy allows the Cloudflare script the bot check loads, so the option works when it is turned on. The Cloudflare entry is the only part of this that takes effect with the option off. diff --git src/hg/lib/cart.c src/hg/lib/cart.c index f004a7f0e4e..f7e6b7bf466 100644 --- src/hg/lib/cart.c +++ src/hg/lib/cart.c @@ -1681,33 +1681,34 @@ static char *getSessionId() /* Get session id if any from CGI. */ { return cgiOptionalString("hgsid"); } void printCaptcha() /* print an html page that shows the captcha and on success, reloads the page with the token added as token=x */ { char *cfSiteKey = cfgVal(CLOUDFLARESITEKEY); if (!cfSiteKey) return; fprintf(stderr, "CAPTCHA_PRINT %s\n", getSessionId()); + cspWriteResponseHeader(); puts("Content-Type:text/html\n"); // puts outputs one newline. Header requires two newlines. puts("<html><head>"); - puts("<script>"); + printf("<script nonce='%s'>\n", getNonce()); printf("function showWidget() { \n" "turnstile.render('#myWidget', {\n" "sitekey: '%s',\n" "theme: 'light',\n" "callback: function (token) {\n" " const parser = new URL(window.location);\n" " parser.searchParams.set('token', token);\n" " window.location = parser.href;\n" " },\n" "});\n" "}\n", cfSiteKey); puts("</script>"); puts("</head><body>"); puts("<style>body, h1, h2, h3, h4, h5, h6 { font-family: Helvetica, Arial, sans-serif; }</style>\n"); puts("<h4>The Genome Browser is protecting itself from bots. This will just take a few seconds.</h4>"); @@ -1790,30 +1791,31 @@ if (userIdNum != 0) { struct sqlConnection *conn = hConnectCentralNoCache(); char *table = cfgOptionDefault("hguidIpTracking.table", "hguidIpAccess"); char query[256]; sqlSafef(query, sizeof(query), "DELETE FROM %s WHERE userId=%lu", table, userIdNum); sqlUpdate(conn, query); sqlDisconnect(&conn); } } return; } else { + cspWriteResponseHeader(); puts("Content-Type: text/html\n"); puts("<html><body>Internal captcha error: Cloudflare rejected the captcha token. " "Something is not working internally, we are very sorry. You can try reloading the page. " "If this problem persists, send an email to genome-www@soe.ucsc.edu and we will " "look into it as quickly as we can in the PST timezone. You can use any internet browser " "where you have used the genome browser before, but not from this internet browser. " "You can try our mirror sites, " "genome-euro.ucsc.edu or genome-asia.ucsc.edu, while we are working on a solution.</body></html>"); fprintf(stderr, "CAPTCHA_REJECT %s\n", getSessionId()); exit(0); } } printCaptcha(); } @@ -2933,30 +2935,31 @@ cartExclude(cart, "verbose"); return cart; } static void addHttpHeaders() /* CGIs can initialize the global variable httpHeaders to control their own HTTP * headers. This allows, for example, to prevent web browser caching of hgTracks * responses, but implicitly allow web browser caching everywhere else */ { struct slPair *h; for (h = httpHeaders; h != NULL; h = h->next) { printf("%s: %s\n", h->name, (char *)h->val); } +cspWriteResponseHeader(); } void cartWriteHeaderAndCont(struct cart* cart, char *cookieName, char *contType) /* write http headers including cookie and content type line. * contType defaults to text/html when NULL. * cookieName defaults to hUserCookie() when NULL */ { /* The CGI header must be written exactly once; a second write lands in the page body. Some flows * (e.g. hgc) emit it early via cartAndCookieWithHtml before a later webStart also asks for it, so * guard here rather than trusting every caller to check cartDidContentType first. */ if (cartDidContentType) return; if (!contType) contType = "text/html"; if (!cookieName)