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.