9ad04e0a0b06ec3c4f09ef1b6c3ce6be79b61c68 braney Sun Aug 16 11:56:56 2026 -0700 cart: validate file names read back out of the cart Several cart variables hold the name of a file the server created for a user. Route them through one shared check, isServerUserFilePath(), which accepts the trash directory, the session-data directories and myVariantsDataDir, and apply it both where values enter the cart and where the file names are used. A few of these variables may instead hold a remote URL. Those get their own list and isServerUserFileOrUrl(), because the code that reads them chooses between a fetch and a local open by looking for a protocol. Consolidates two hand-rolled copies of the same test in blatShare.c and customFactory.c, and drops the weaker private copy in sessionData.c. Adds hg/utils/cartFileVarCatalog, a registry that scans the tree for a cart value reaching a file call and reconciles what it finds against the lists in cart.c, so a new one of these cannot be added without somebody noticing. Its --reconcile is quiet enough for the nightly cron the other catalogs use, and it is what turned up seven of the names now on those lists. refs #37623 diff --git src/hg/lib/cart.c src/hg/lib/cart.c index 12d9b70b15f..c0c990defee 100644 --- src/hg/lib/cart.c +++ src/hg/lib/cart.c @@ -29,30 +29,33 @@ #include "geoMirror.h" #include "hubConnect.h" #include "trackHub.h" #include "cgiApoptosis.h" #include "customComposite.h" #include "regexHelper.h" #include "windowsToAscii.h" #include "jsonWrite.h" #include "verbose.h" #include "genark.h" #include "quickLift.h" #include "botDelay.h" #include "curlWrap.h" #include "hubSpaceKeys.h" #include "myVariantsShare.h" +#include "customTrack.h" +#include "dupTrack.h" +#include "myVariants.h" static char *sessionVar = "hgsid"; /* Name of cgi variable session is stored in. */ static char *positionCgiName = "position"; DbConnector cartDefaultConnector = hConnectCart; DbDisconnect cartDefaultDisconnector = hDisconnectCart; static boolean cartDidContentType = FALSE; struct slPair *httpHeaders = NULL; // A list of headers to output before the content-type static void hashUpdateDynamicVal(struct hash *hash, char *name, void *val) /* Val is a dynamically allocated (freeMem-able) entity to put * in hash. Override existing hash item with that name if any. * Otherwise make new hash item. */ { @@ -183,47 +186,159 @@ while ((helOverlay = hashNext(&cookie)) != NULL) { char *varName = helOverlay->name; struct hashEl *helOrig = hashLookup(origHash, varName); if (helOrig) { // we don't want to hide a track that's visible in the overlay if (differentString("hide", helOverlay->val)) hashReplace(origHash, helOverlay->name, helOverlay->val); } else hashAdd(origHash, varName, helOverlay->val); } } +/* Cart variables below hold the name of a file that the server itself made for this user, + * either in the trash directory or in the durable session-data directory that a saved + * session's trash files get moved to. Each of these names reaches an open, a read or a + * delete somewhere in the tree, and a cart value is not ours to trust: values arrive from + * CGI parameters, from an uploaded or fetched hgSession settings file, and from another + * user's shared session. So a value that names some other file is dropped on the way in, + * by cartValueIsAcceptable() below. + * + * Add a name here when a new cart variable comes to hold a server-side file name. Keep + * validating at the point of use as well, with isServerUserFilePath(): some file name + * variables are named by a trackDb setting rather than by the code (speciesUseFile), so no + * fixed list can cover them. + * + * hg/utils/cartFileVarCatalog checks these arrays for completeness: it scans the tree for a + * cart value reaching a file call and fails if it finds one that is neither listed here nor + * described in the catalog. A name added here wants a row there as well. */ + +static char *fileNameCartVars[] = +{ + "hgta_userRegionsFile", // user regions, hgTables.h hgtaUserRegionsFile, also hgIntegrator + "hgta_identifierFile", // hgTables pasted identifier list, hgTables.h hgtaIdentifierFile + DUP_TRACKS_VAR, // duplicated track stanzas + "blatLastBigBed", // BLAT bigPsl, see blatFindPinnedBigPsl() + "blatPslFile", // saved BLAT result, see hgBlat doRedisplayResults() + "blatFaFile", // the query sequence that goes with blatPslFile + "hgg_mrnaFoldPs", // hgGene mRNA fold PostScript, hgGene.h hggMrnaFoldPs + "hgp_matchFile", // hgVisiGene search matches, hgVisiGene.h hgpMatchFile + "near.customFile", // hgNear custom column file, hgNear.h customFileVarName + "gsTemp", // the file hgTables uploads to GenomeSpace +}; + +static char *fileNameCartVarPrefixes[] = +{ + CT_FILE_VAR_PREFIX, // ctfile_, ctfile_hub_: custom tracks + MYVARIANTS_FILE_VAR_PREFIX, // mvCtfile_: myVariants custom track + customCompositeCartName "-", // customComposite-: track collection hub + quickLiftCartName "-", // hubQuickLift-: quickLift hub +}; + +/* These two hold either a remote URL or the name of a file the server made, and the code that + * reads them tells the cases apart by looking for a protocol. A value with no protocol falls + * through to opening a local file, so it has to be one of ours; a real URL is fine. */ + +static char *urlOrFileNameCartVars[] = +{ + "multiRegionsBedUrl", // hgTracks multi-region BED: a URL, or the trash file we wrote + hgsLoadUrlName, // hgSession load settings from URL +}; + +static boolean cartVarHoldsFileName(char *var) +/* Return TRUE if var is one of the cart variables listed above. */ +{ +int i; +for (i = 0; i < ArraySize(fileNameCartVars); i++) + if (sameString(var, fileNameCartVars[i])) + return TRUE; +for (i = 0; i < ArraySize(fileNameCartVarPrefixes); i++) + if (startsWith(fileNameCartVarPrefixes[i], var)) + return TRUE; +return FALSE; +} + +static boolean cartVarHoldsUrlOrFileName(char *var) +/* Return TRUE if var is one of the cart variables that may hold either. */ +{ +int i; +for (i = 0; i < ArraySize(urlOrFileNameCartVars); i++) + if (sameString(var, urlOrFileNameCartVars[i])) + return TRUE; +return FALSE; +} + +static void logDroppedFileNameVar(char *var, char *why) +/* Note the drop in the error log, so that a false positive can be spotted after release. + * The variable name comes from the user, so copy out only characters that cannot forge a + * log line of their own, and keep the copy short. */ +{ +char clean[129]; +int i; +for (i = 0; i < (int)sizeof(clean) - 1 && var[i] != 0; i++) + { + unsigned char c = var[i]; + clean[i] = (isalnum(c) || c == '_' || c == '.' || c == '-') ? c : '?'; + } +clean[i] = 0; +fprintf(stderr, "cart: dropped %s, value is not %s\n", clean, why); +} + +static boolean cartValueIsAcceptable(char *var, char *val) +/* Return TRUE unless var names a server-created file and val names something else. An empty + * value is fine; the CGIs treat it as "no file" and several saved sessions carry one. */ +{ +if (isEmpty(val)) + return TRUE; +if (cartVarHoldsFileName(var)) + { + if (isServerUserFilePath(val)) + return TRUE; + logDroppedFileNameVar(var, "a trash or session-data file name"); + return FALSE; + } +if (cartVarHoldsUrlOrFileName(var)) + { + if (isServerUserFileOrUrl(val)) + return TRUE; + logDroppedFileNameVar(var, "a URL or a trash or session-data file name"); + return FALSE; + } +return TRUE; +} + static void loadHash(struct hash *hash, char *contents) /* Load a hash from a cart-like string. */ { char *namePt, *dataPt, *nextNamePt; namePt = contents; while (namePt != NULL && namePt[0] != 0) { dataPt = strchr(namePt, '='); if (dataPt == NULL) errAbort("Mangled input string %s", namePt); *dataPt++ = 0; nextNamePt = strchr(dataPt, '&'); if (nextNamePt == NULL) nextNamePt = strchr(dataPt, ';'); /* Accomodate DAS. */ if (nextNamePt != NULL) *nextNamePt++ = 0; cgiDecode(dataPt,dataPt,strlen(dataPt)); + if (cartValueIsAcceptable(namePt, dataPt)) hashAdd(hash, namePt, cloneString(dataPt)); namePt = nextNamePt; } } void cartParseOverHashExt(struct cart *cart, char *contents, boolean merge) /* Parse cgi-style contents into a hash table. If merge is FALSE, this will *not* * replace existing members of hash that have same name, so we can * support multi-select form inputs (same var name can have multiple * values which will be in separate hashEl's). If merge is TRUE, we * replace existing values with new values */ { if (merge) { struct hash *newHash = newHash(8); @@ -589,30 +704,32 @@ char *multVar = cv->name + multSize; if (! cgiVarExists(multVar)) { storeInOldVars(cart, oldVars, multVar); storeInOldVars(cart, oldVars, cv->name); cartRemove(cart, multVar); } } } /* Handle non-boolean vars. */ for (cv = cgiVarList(); cv != NULL; cv = cv->next) { if (! (startsWith(booShadow, cv->name) || hashLookup(booHash, cv->name))) { + if (!cartValueIsAcceptable(cv->name, cv->val)) + continue; // leave the file name the server itself stored in the cart alone storeInOldVars(cart, oldVars, cv->name); cartRemove(cart, cv->name); if (differentString(cv->val, CART_VAR_EMPTY)) // NOTE: CART_VAR_EMPTY logic not implemented for boolShad hashAdd(cgiHash, cv->name, cv->val); } } /* Add new settings to cart (old values of these variables have been * removed above). */ struct hashEl *hel = hashElListHash(cgiHash); while (hel != NULL) { cartAddString(cart, hel->name, hel->val); hel = hel->next; } @@ -1234,30 +1351,31 @@ } if (addToCart) { if (val != NULL) { size_t valLen = strlen(val); if (valLen > CART_VAL_MAX_LENGTH) { addToCart = FALSE; vsGotValTooLong(stats, var, valLen); } else { if (decodeVal) decodeForHgSession(val); + if (cartValueIsAcceptable(var, val)) cartAddString(cart, var, val); updatePrevVar(pPrevVar, var); } } else if (var != NULL) { cartSetString(cart, var, ""); updatePrevVar(pPrevVar, var); } } return addToCart; } boolean cartLoadSettingsFromUserInput(struct lineFile *lf, struct cart *cart, struct hash *oldVars, char *actionVar, struct dyString *dyMessage) @@ -1741,30 +1859,34 @@ if (cartVarExists(cart, hgsDoOtherUser)) { char *otherUser = cartString(cart, hgsOtherUserName); char *sessionName = cartString(cart, hgsOtherUserSessionName); boolean mergeCart = cartUsualBoolean(cart, hgsMergeCart, FALSE); struct sqlConnection *conn2 = hConnectCentral(); cartLoadUserSessionExt(conn2, otherUser, sessionName, cart, oldVars, hgsDoOtherUser, mergeCart); hDisconnectCentral(&conn2); cartTrace(cart, "after cartLUS", conn); didSessionLoad = TRUE; } else if (cartVarExists(cart, hgsDoLoadUrl)) { char *url = cartString(cart, hgsLoadUrlName); + /* netUrlOpen() treats a string with no protocol as a local path and open()s it + * (lib/net.c), so this has to be a URL or a file we made before it is opened. */ + if (!isServerUserFileOrUrl(url)) + errAbort("Can only load session settings from a URL."); struct lineFile *lf = netLineFileOpen(url); struct dyString *dyMessage = dyStringNew(0); boolean ok = cartLoadSettingsFromUserInput(lf, cart, oldVars, hgsDoLoadUrl, dyMessage); lineFileClose(&lf); cartTrace(cart, "after cartLS", conn); if (! ok) { warn("Unable to load session file: %s", dyMessage->string); } didSessionLoad = ok; dyStringFree(&dyMessage); } } #endif /* GBROWSE */ @@ -3152,30 +3274,35 @@ cartDefaultDisconnector = disconnector; } char *cartGetOrderFromFile(char *genomeDb, struct cart *cart, char *speciesUseFile) /* Look in a cart variable that holds the filename that has a list of * species to show in a maf file */ { char *val; struct dyString *orderDY = dyStringNew(256); char *words[16]; if ((val = cartUsualString(cart, speciesUseFile, NULL)) == NULL) { errAbort("can't find species list file var '%s' in cart\n",speciesUseFile); } +/* speciesUseFile names the cart variable, and a trackDb or hub author picks that name, so no + * fixed list in the cart can screen this one -- check the value here instead. */ +if (!isServerUserFilePath(val)) + errAbort("species list file var '%s' does not name a file we made\n", speciesUseFile); + struct lineFile *lf = lineFileOpen(val, TRUE); if (lf == NULL) errAbort("can't open species list file %s",val); while( ( lineFileChopNext(lf, words, sizeof(words)/sizeof(char *)) )) dyStringPrintf(orderDY, "%s ",words[0]); return dyStringCannibalize(&orderDY); } char *cartLookUpVariableClosestToHome(struct cart *cart, struct trackDb *tdb, boolean parentLevel, char *suffix,char **pVariable) /* Returns value or NULL for a cart variable from lowest level on up. Optionally * fills the non NULL pVariable with the actual name of the variable in the cart */