934a51aa2ebc7e11bfba57804642cd03e4e1e60a
braney
  Tue Aug 18 11:45:11 2026 -0700
trackHub: tighten group name validation, refs #38123

diff --git src/hg/hgTracks/hgTracks.c src/hg/hgTracks/hgTracks.c
index 71f2ae89df5..422915607e3 100644
--- src/hg/hgTracks/hgTracks.c
+++ src/hg/hgTracks/hgTracks.c
@@ -8883,31 +8883,33 @@
 if (sameOk(track->groupName, "user") || sameOk(track->groupName, "blat"))
     printTrackDelIcon(track);
 
 char *quickLiftSourceDb = (track->tdb != NULL) ?
         trackDbSetting(track->tdb, "quickLiftDb") : NULL;
 /* skip the synthetic "Alignment Differences" track -- it's added at runtime
  * by hubConnect, not from the hub file, so removing it from the file is a
  * no-op and it would reappear on the next page load. */
 if (quickLiftSourceDb != NULL &&
     (track->tdb->type == NULL || !startsWith("bigQuickLiftChain", track->tdb->type)))
     printQuickLiftDelIcon(track, quickLiftSourceDb);
 
 if (track->hasUi)
     {
     char *url = trackUrl(track->track, chromName);
-    char *longLabel = replaceChars(track->longLabel, "\"", """);
+    // longLabel comes from trackDb, which a track hub controls, so encode it to match the
+    // shortLabel below.
+    char *longLabel = htmlEncode(track->longLabel);
 
     struct dyString *dsMouseOver = dyStringCreate("%s", longLabel);
     struct trackDb *tdb = track->tdb;
 
     if (tdbIsSuper(tdb))
         dyStringPrintf(dsMouseOver, " - container, %d tracks ", slCount(tdb->children));
     else if (tdbIsComposite(tdb))
         dyStringPrintf(dsMouseOver, " - container, %d subtracks ", slCount(tdb->subtracks));
 
     // Print icons before the title when any are defined
     hPrintIcons(track->tdb);
 
     hPrintf("<A class='trackLink' HREF=\"%s\" data-group='%s' data-track='%s' title=\"%s\">", url, track->groupName, track->track, dyStringCannibalize(&dsMouseOver));
 
     freeMem(url);