6d4c020673160d1f301931782f56085fff8967ae
markd
  Fri Sep 18 12:35:14 2026 -0700
Fix hgTrackUi 400 when fetching faceted composite metadata on a curated hub assembly. refs #38384

handleFileFetch authorizes a fileUrl either by it sitting under a connected
hub's hub.txt directory, or by it matching a whitelisted trackDb setting
(metaDataUrl, colorSettingsUrl).  The second check was gated on the track not
being a hub track, because user hub settings could point anywhere.

On a curated hub assembly such as hs1 both checks failed: the track name is
hub-prefixed, so the whitelist was skipped, and the metadata sits at
/gbdb/hs1/proCapNet/, outside the hub.txt directory /gbdb/hs1/hubs/alpha/.
The ProCapNet config page showed "Error loading metadata: HTTP Status: 400"
in place of the faceted table.  The same track on hg38, a native database,
worked.

A curated hub's trackDb is admin-written and as trustworthy as a native
track's, so let the whitelist check run for it.  New trackIsFromCuratedHub
matches the track's own hub id against the hub url dbDb names for the
assembly, so a user hub attached to the same assembly still does not qualify.

Claude-Session: https://claude.ai/code/session_01LAB6jWshLvB7eNXQKWVuW5

diff --git src/hg/hgTrackUi/hgTrackUi.c src/hg/hgTrackUi/hgTrackUi.c
index 711f96846c5..d160d94b778 100644
--- src/hg/hgTrackUi/hgTrackUi.c
+++ src/hg/hgTrackUi/hgTrackUi.c
@@ -4406,99 +4406,123 @@
    else if (sameString(operation, "undupe"))
        {
        newTrack = dupTrackSkipToSourceName(track);
        undupTrackInCartAndTrash(track, cart);
        }
    else
        {
        internalErr();
        }
    cartRemove(cart, opVar);
    }
 return newTrack;
 }
 
 /* Setting names whose file contents are safe to serve via hgFetch.
- * Only admin-configured (native track) values are checked -- never hub or custom tracks.
+ * Only admin-configured values are checked -- native tracks and curated hubs,
+ * never user hubs or custom tracks.
  * Do NOT add bigDataUrl or bigDataIndex here -- those may be restricted (we
  * might change this later to instead respect the tableBrowser setting in trackDb). */
 static char *fetchableSettings[] = {"metaDataUrl", "colorSettingsUrl", NULL};
 
 boolean fileUrlMatchesHub(char *fileUrl, struct hubConnectStatus *hubStatus)
 /* Ignores fetchableSettings for now, whitelisting anything that sits inside
  * the hub.txt directory structure.  Assumes fileUrl has been canonicalized. */
 {
 char baseDir[2048];
 splitPath(hubStatus->hubUrl, baseDir, NULL, NULL);
 return startsWith(baseDir, fileUrl);
 }
 
 static boolean fileUrlMatchesTrackSetting(char *fileUrl, struct trackDb *tdb)
 /* Check if fileUrl matches any whitelisted setting in this trackDb.
  * Assumes fileUrl has been canonicalized. */
 {
 char **p;
 for (p = fetchableSettings; *p != NULL; p++)
     {
     char *val = trackDbSetting(tdb, *p);
     if (val != NULL && sameString(val, fileUrl))
         return TRUE;
     }
 return FALSE;
 }
 
+static boolean trackIsFromCuratedHub(char *db, char *track,
+                                     struct hubConnectStatus *hubStatusList)
+/* Check if a hub track comes from the curated hub that dbDb names for this assembly.
+ * A curated hub such as hs1 keeps its data outside the hub.txt directory, so
+ * fileUrlMatchesHub rejects it, but its trackDb is admin-configured and as
+ * trustworthy as a native track's.  A user hub attached to the same assembly is
+ * not, hence the match against the one hub dbDb names. */
+{
+char *curatedUrl = NULL;
+if (!hubConnectGetCuratedUrl(trackHubSkipHubName(db), &curatedUrl) || isEmpty(curatedUrl))
+    return FALSE;
+curatedUrl = hReplaceGbdb(curatedUrl);
+unsigned hubId = hubIdFromTrackName(track);
+struct hubConnectStatus *hubStatus;
+for (hubStatus = hubStatusList; hubStatus != NULL; hubStatus = hubStatus->next)
+    {
+    if (hubStatus->id == hubId)
+        return sameString(hubStatus->hubUrl, curatedUrl);
+    }
+return FALSE;
+}
+
 void handleFileFetch(struct cart *cart)
 /* Checks if a requested file is a legal request based on an attached cart or
  * native track.  If so, retrieves the file content via UDC and retransmits
  * it as the page content. */
 {
 char *genome = NULL;
 getDbAndGenome(cart, &database, &genome, NULL);
 initGenbankTableNames(database);
 
 char *fileUrl = cartOptionalString(cart, "fileUrl");
 char *urlClone = cloneString(fileUrl);
 cgiDecode(urlClone, urlClone, strlen(urlClone));
 fileUrl = resolveDotDots(urlClone);
 freeMem(urlClone);
 
 boolean matchFound = FALSE;
 
 // Check if fileUrl falls under a connected hub's base directory
 struct hubConnectStatus *hubStatusList = hubConnectStatusListFromCartAll(cart);
 struct hubConnectStatus *hubStatus = hubStatusList;
 while (hubStatus != NULL)
     {
     if (isEmpty(hubStatus->errorMessage) && fileUrlMatchesHub(fileUrl, hubStatus))
         {
         matchFound = TRUE;
         break;
         }
     hubStatus = hubStatus->next;
     }
 
-// For native database tracks (not hub or custom tracks), check if fileUrl matches
-// a whitelisted trackDb setting.  Only native tracks are checked here because their
-// settings are admin-configured and trusted.  Hub and custom track settings are
-// user-controlled and could be used for SSRF attacks.
+// For native database tracks and curated hub tracks, check if fileUrl matches a
+// whitelisted trackDb setting.  Only these are checked here because their settings are
+// admin-configured and trusted.  User hub and custom track settings are user-controlled
+// and could be used for SSRF attacks.
 if (!matchFound)
     {
     char *track = cartOptionalString(cart, "track");
     char *sourceDb = cartOptionalString(cart, "sourceDb"); // for future quickLift use
     if (sourceDb == NULL)
         sourceDb = database;
-    if (track != NULL && !isHubTrack(track) && !isCustomTrack(track))
+    if (track != NULL && !isCustomTrack(track) &&
+        (!isHubTrack(track) || trackIsFromCuratedHub(sourceDb, track, hubStatusList)))
         {
         struct trackDb *tdb = tdbForTrack(sourceDb, track, NULL);
         if (tdb != NULL)
             matchFound = fileUrlMatchesTrackSetting(fileUrl, tdb);
         }
     }
 
 if (!matchFound)
     {
     puts("Status: 400 Bad Request");
     errAbort("Supplied fileUrl does not match any connected hubs or track settings.");
     }
 
 // By now we know that fileUrl points to something valid to fetch and return to the user.
 // Now we just have to fetch the file contents and retransmit it.