2cdae04ddc1e46cdfc2e5e8cf479cbbbb616ad10
chmalee
  Tue Sep 22 15:55:10 2026 -0700
htmlSanitize tooltips before printing them into the page, refs #38226

diff --git src/hg/hgTracks/imageV2.c src/hg/hgTracks/imageV2.c
index 8b4cc643d0d..3f05652be00 100644
--- src/hg/hgTracks/imageV2.c
+++ src/hg/hgTracks/imageV2.c
@@ -5,30 +5,31 @@
 #include "common.h"
 #include "hPrint.h"
 #include "chromInfo.h"
 #include "hdb.h"
 #include "hui.h"
 #include "jsHelper.h"
 #include "cheapcgi.h"
 #include "htmshell.h"
 #include "imageV2.h"
 #include "hgTracks.h"
 #include "hgConfig.h"
 #include "regexHelper.h"
 #include "customComposite.h"
 #include "chromAlias.h"
 #include "trackHub.h"
+#include "htmlSanitize.h"
 
 // Note: when right-click View image (or pdf output) then theImgBox==NULL, so it will be rendered as a single simple image
 struct imgBox   *theImgBox   = NULL; // Make this global for now to avoid huge rewrite
 struct imgTrack *curImgTrack = NULL; // Make this global for now to avoid huge rewrite
 
 /////////////////////////
 // FLAT TRACKS
 // A simplistic way of flattening the track list before building the image
 // NOTE: Strategy is NOT to use imgBox->imgTracks, since this should be independednt of imageV2
 /////////////////////////
 void flatTracksAdd(struct flatTracks **flatTracks,struct track *track,struct cart *cart, struct slName *orderedWiggles)
 // Adds one track into the flatTracks list
 {
 struct flatTracks *flatTrack;
 AllocVar(flatTrack);
@@ -1885,46 +1886,56 @@
         else if (startsWith("/cgi-bin/hgGene", item->linkVar)) // redmine #4151
             hPrintf(" HREF='..%s'",item->linkVar);             // FIXME: Chin should get rid
         else                                                   // of this special case!
             hPrintf(" HREF='%s'",item->linkVar);
         hPrintf(" class='area'");
         }
     else
         warn("map item has no url!");
 
     if (item->title != NULL && strlen(item->title) > 0)
         {
         char *encodedString = attributeEncode(item->title);
         if (cfgOptionBooleanDefault("showMouseovers", FALSE))
             {
             if (isNotEmpty(item->tooltip))
-                hPrintf(" title='%s' data-tooltip='%s'", encodedString, attributeEncode(item->tooltip));
+                {
+                char *sanitized = htmlSanitize(item->tooltip);
+                char *encoded = attributeEncode(sanitized);
+                hPrintf(" title='%s' data-tooltip='%s'", encodedString, encoded);
+                freeMem(encoded);
+                freeMem(sanitized);
+                }
             else
                 hPrintf(" TITLE='%s'", encodedString);
             }
         else
             {
             // for TITLEs, which we use for mouseOvers,  since they can't have HTML in
             // them, we substitute a unicode new line for <br> after we've encoded it.
             // This is stop-gap until we start doing mouseOvers entirely in Javascript
             hPrintf(" TITLE='%s'", replaceChars(encodedString,"&#x3C;br&#x3E;", "&#8232;"));
             }
         }
     else if (isNotEmpty(item->tooltip) && cfgOptionBooleanDefault("showMouseovers", FALSE))
         {
         // some items have no title string (no item name) but do have tooltips
-        hPrintf(" data-tooltip='%s'", attributeEncode(item->tooltip));
+        char *sanitized = htmlSanitize(item->tooltip);
+        char *encoded = attributeEncode(sanitized);
+        hPrintf(" data-tooltip='%s'", encoded);
+        freeMem(encoded);
+        freeMem(sanitized);
         }
     if (item->id != NULL)
         hPrintf(" id='%s'", item->id);
     hPrintf(">" );
     }
 hPrintf("</MAP>\n");
 return TRUE;
 }
 
 static void imageDraw(struct imgBox *imgBox,struct imgTrack *imgTrack,struct imgSlice *slice,
                       char *name,int offsetX,int offsetY,boolean useMap)
 // writes an image as HTML
 {
 if (slice->parentImg && slice->parentImg->file != NULL)
     {
@@ -2080,45 +2091,49 @@
 struct mapSet *map = sliceGetMap(slice,FALSE); // Could be the image map or slice specific
 if (map)
     useMap = imageMapDraw(map,name);
 else if (slice->link != NULL)
     {
     if (sameString(TITLE_BUT_NO_LINK,slice->link))
         { // This fake link ensures a mouse-over title is seen but not heard
         hPrintf("<A class='%s'",TITLE_BUT_NO_LINK);
         }
     else if (skipToSpaces(slice->link) != NULL)
         hPrintf("  <A HREF=%s",slice->link);
     else
         hPrintf("  <A HREF='%s'",slice->link);
     if (slice->title != NULL)
         {
+        char *sanitized = htmlSanitize(slice->title);
+        char *encSanitized = attributeEncode(sanitized);
         if (sliceType == stButton)
             {
             enum browserType browser = cgiClientBrowser(NULL,NULL,NULL);
             char *newLine = NEWLINE_TO_USE(browser);
             char *ellipsis = ELLIPSIS_TO_USE(browser);
             if (imgTrack->reorderable)
                 hPrintf(" TITLE='%s%sclick or right click to configure%s%sdrag to reorder%s'",
-                        attributeEncode(slice->title), newLine, ellipsis, newLine,
+                        encSanitized, newLine, ellipsis, newLine,
                         (tdbIsCompositeChild(imgTrack->tdb) ? " highlighted subtracks" : "") );
             else
                 hPrintf(" TITLE='%s%sclick or right click to configure%s'",
-                        attributeEncode(slice->title), newLine, ellipsis);
+                        encSanitized, newLine, ellipsis);
             }
         else
-            hPrintf(" TITLE='Click for: &#x0A;%s'", attributeEncode(slice->title) );
+            hPrintf(" TITLE='Click for: &#x0A;%s'", encSanitized );
+        freeMem(sanitized);
+        freeMem(encSanitized);
         }
     hPrintf(">\n" );
     }
 
 char *trackName = (imgTrack->name != NULL ? imgTrack->name : imgTrack->tdb->track );
 struct jsonElement *trackHash = jsonFindNamedField(ele, "trackDb", trackName);
 jsonObjectAdd(trackHash, "imgOffsetY", newJsonNumber(offsetY));
 imageDraw(imgBox,imgTrack,slice,name,offsetX,offsetY,useMap);
 if (slice->link != NULL)
     hPrintf("</A>");
 
 if (slice->parentImg)
     hPrintf("</div>");
 }
 
@@ -2229,75 +2244,83 @@
 jsonTdbSettingsInit(jsonTdbVars);
 
 char *newLine = NEWLINE_TO_USE(cgiClientBrowser(NULL,NULL,NULL));
 struct imgTrack *imgTrack = smashSquish(imgBox->imgTracks);
 for (;imgTrack!=NULL;imgTrack=imgTrack->next)
     {
     char *trackName = (imgTrack->name != NULL ? imgTrack->name : imgTrack->tdb->track );
     struct track *track = hashFindVal(trackHash, trackName);
     if (track)
         jsonTdbSettingsBuild(jsonTdbVars, track, TRUE);
     hPrintf("<TR id='tr_%s' abbr='%d' class='imgOrd%s%s%s'>\n",trackName,imgTrack->order,
             (imgTrack->reorderable ? " trDraggable" : " nodrop nodrag"),
             (imgTrack->centerLabelSeen != clAlways ? " clOpt" : ""),
             (imgTrack->ajaxRetrieval ? " mustRetrieve" : ""));
 
+    char *sanitized = NULL, *encSanitized = NULL;
+    if (imgTrack->reorderable)
+        {
+        sanitized = htmlSanitize(imgTrack->tdb->longLabel);
+        encSanitized = attributeEncode(sanitized);
+        }
     if (imgBox->showSideLabel && imgBox->plusStrand)
         {
         // button
         safef(name, sizeof(name), "btn_%s", trackName);
         hPrintf(" <TD id='td_%s'%s>\n",name,(imgTrack->reorderable ? " class='dragHandle'" : ""));
         sliceAndMapDraw(imgBox,imgTrack,stButton,name,FALSE, jsonTdbVars);
         hPrintf("</TD>\n");
         // leftLabel
         safef(name,sizeof(name),"side_%s",trackName);
         if (imgTrack->reorderable)
             hPrintf(" <TD id='td_%s' class='dragHandle tdLeft' title='%s%sdrag to reorder'>\n",
-                    name,attributeEncode(imgTrack->tdb->longLabel),newLine);
+                    name,encSanitized,newLine);
         else
             hPrintf(" <TD id='td_%s' class='tdLeft'>\n",name);
         sliceAndMapDraw(imgBox,imgTrack,stSide,name,FALSE, jsonTdbVars);
         if (cfgOptionBooleanDefault("greyBarIcons", TRUE))
             hPrintf("<span id='close_btn_%s' title='Hide track' class='hgTracksCloseIcon ui-icon ui-icon-close' style='display: none'></span>", trackName);
         hPrintf("</TD>\n");
         }
 
     // Main/Data image region
     hPrintf(" <TD id='td_data_%s' title='click & drag to scroll; shift+click & drag to zoom'"
             " width=%d class='tdData'>\n", trackName, imgBox->width);
     // centerLabel
     if (imgTrack->hasCenterLabel)
         {
         safef(name, sizeof(name), "center_%s", trackName);
         sliceAndMapDraw(imgBox,imgTrack,stCenter,name,TRUE, jsonTdbVars);
         hPrintf("\n");
         }
     // data image
     safef(name, sizeof(name), "data_%s", trackName);
     sliceAndMapDraw(imgBox,imgTrack,stData,name,(imgTrack->order>0), jsonTdbVars);
     hPrintf("</TD>\n");
 
     if (imgBox->showSideLabel && !imgTrack->plusStrand)
         {
         // rightLabel
         safef(name, sizeof(name), "side_%s", trackName);
         if (imgTrack->reorderable)
             hPrintf(" <TD id='td_%s' class='dragHandle tdRight' title='%s%sdrag to reorder'>\n",
-                    name,attributeEncode(imgTrack->tdb->longLabel),newLine);
+                    name,encSanitized,newLine);
         else
             hPrintf(" <TD id='td_%s' class='tdRight'>\n",name);
         sliceAndMapDraw(imgBox,imgTrack,stSide,name,FALSE, jsonTdbVars);
         if (cfgOptionBooleanDefault("greyBarIcons", TRUE))
             hPrintf("<span id='close_btn_%s' title='Hide track' class='hgTracksCloseIcon ui-icon ui-icon-close' style='display: none'></span>", trackName);
         hPrintf("</TD>\n");
         // button
         safef(name, sizeof(name), "btn_%s", trackName);
         hPrintf(" <TD id='td_%s'%s>\n",name,(imgTrack->reorderable ? " class='dragHandle'" : ""));
         sliceAndMapDraw(imgBox,imgTrack,stButton, name,FALSE, jsonTdbVars);
         hPrintf("</TD>\n");
         }
+    freeMem(encSanitized);
+    freeMem(sanitized);
     hPrintf("</TR>\n");
     }
 hPrintf("</TABLE>\n");
 hPrintf("<!-- - - - - - - - ^^^ IMAGEv2 ^^^ - - - - - - - -->\n");
 jsonTdbSettingsUse(jsonTdbVars);
 }