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/hgTables/userRegions.c src/hg/hgTables/userRegions.c
index 74480fedf03..bd72b6ae8b1 100644
--- src/hg/hgTables/userRegions.c
+++ src/hg/hgTables/userRegions.c
@@ -1,231 +1,236 @@
 /* identifiers - handle identifier lists: uploading, pasting,
  * and restricting to just things on the list. */
 
 /* Copyright (C) 2011 The Regents of the University of California 
  * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */
 
 #include "common.h"
 #include "linefile.h"
 #include "hash.h"
 #include "cheapcgi.h"
 #include "htmshell.h"
 #include "cart.h"
 #include "jksql.h"
 #include "trackDb.h"
 #include "portable.h"
 #include "hgTables.h"
 #include "trashDir.h"
 #include "hui.h"
 #include "obscure.h"
 #include "userRegions.h"
 #include "web.h"
 
 static int maxRegions = 1000;
 static int maxErrors = 100;
 
 void doSetUserRegionsAfterOpen(struct sqlConnection *conn)
 /* Respond to set regions button. */
 {
 char *oldPasted = cartUsualString(cart, hgtaEnteredUserRegions, "");
 char *db = cartOptionalString(cart, hgtaUserRegionsDb);
 if (db && !sameString(db, database))
     oldPasted = "";
 hPrintf("<FORM ACTION=\"%s\" METHOD=%s "
         " ENCTYPE=\"multipart/form-data\" NAME=\"mainForm\">\n", getScriptName(),
         cartUsualString(cart, "formMethod", "POST"));
 cartSaveSession(cart);
 hPrintf("<TABLE><TR><TD ALIGN=LEFT>\n");
 hPrintf("Paste regions:");
 hPrintf("</TD><TD ALIGN=RIGHT>");
 hPrintf("Or upload file: <INPUT TYPE=FILE NAME=\"%s\" aria-label='Upload region definitions file'>"
 	"&nbsp;<BR>\n",
 	hgtaEnteredUserRegionFile);
 hPrintf("</TD></TR><TR><TD COLSPAN=2 ALIGN=LEFT>\n");
 htmlPrintf("<TEXTAREA NAME='%s|attr|' ROWS=10 COLS=70 aria-label='Paste regions'>%s</TEXTAREA>",
     hgtaEnteredUserRegions, (oldPasted != NULL ? oldPasted : ""));
 hPrintf("</TD></TR><TR><TD COLSPAN=2 ALIGN=LEFT>\n");
 cgiMakeButton(hgtaDoSubmitUserRegions, "Submit");
 hPrintf("&nbsp;");
 cgiMakeButton(hgtaDoClearSetUserRegionsText, "Clear");
 hPrintf("&nbsp;");
 cgiMakeButton(hgtaDoMainPage, "Cancel");
 hPrintf("</TD></TR></TABLE>");
 hPrintf("</FORM><BR>\n");
 webIncludeHelpFile("hgTbUserRegionsHelp", FALSE);
 }
 
 void doSetUserRegions(struct sqlConnection *conn)
 /* Respond to set regions button. */
 {
 htmlOpen("Enter region definition\n");
 doSetUserRegionsAfterOpen(conn);
 htmlClose();
 }
 
 static char *limitText(char *text)
 /* read text string and limit to maxRegions actual data lines */
 {
 struct dyString *limitedText = dyStringNew(0);
 /* Even if using FALSE for zTerm, lineFile still does a memmove when it hits the end
  * and thus clobbers the string, so call lineFileOnString on a copy: */
 char copy[strlen(text)+1];
 safecpy(copy, sizeof(copy), text);
 struct lineFile *lf = lineFileOnString("limitText", FALSE, copy);
 char *lineStart = NULL;
 int lineLength = 0;
 int legitimateLineCount = 0;
 while (legitimateLineCount < maxRegions && lineFileNext(lf, &lineStart, &lineLength))
     {
     char *s, c;
     s = skipLeadingSpaces(lineStart);
     c = s[0];
     if (c != 0 && c != '#')
 	++legitimateLineCount;
     dyStringAppendN(limitedText, lineStart, lineLength);
     }
 if ((legitimateLineCount == maxRegions) && lineFileNext(lf, &lineStart, &lineLength))
     warn("WARNING: defined regions limit of %d definitions reached at line %d<BR>\n",
          maxRegions, lf->lineIx-1);
 lineFileClose(&lf);
 return (dyStringCannibalize(&limitedText));
 }
 
 static void cartRemoveUserRegions()
 /* Remove all cart variables related to storage of user regions. */
 {
 cartRemove(cart, hgtaEnteredUserRegions);
 cartRemove(cart, hgtaEnteredUserRegionFile);
 cartRemove(cart, hgtaUserRegionsFile);
 cartRemove(cart, hgtaUserRegionsDb);
 cartRemove(cart, hgtaRegionType);
 }
 
 void doSubmitUserRegions(struct sqlConnection *conn)
 /* Process submit in set regions page. */
 {
 char *idText = trimSpaces(cartString(cart, hgtaEnteredUserRegions));
 char *userRegionFile = trimSpaces(cartString(cart, hgtaEnteredUserRegionFile));
 
 htmlOpen("Table Browser (Region definitions)");
 
 /* presence of fileName text overrides previously existing text area
  *	contents
  */
 if (userRegionFile != NULL && userRegionFile[0] != 0)
     {
     idText = cloneString(userRegionFile);
     cartRemove(cart, hgtaEnteredUserRegions);
     cartRemove(cart, hgtaUserRegionsFile);
     cartSetString(cart, hgtaEnteredUserRegions, idText);
     }
 
 char *lineLimitText = limitText(idText);
 if ( (strlen(lineLimitText) > 0) && (strlen(lineLimitText) != strlen(idText)) )
     {
     idText = lineLimitText;
     cartSetString(cart, hgtaEnteredUserRegions, lineLimitText);
     }
 else
     freeMem(lineLimitText);
 
 boolean success = TRUE;
 if (isNotEmpty(idText))
     {
     int regionCount = 0;
     char *warnText = NULL;
     char *trashFileName = userRegionsParse(database, idText, maxRegions, maxErrors,
                                            &regionCount, &warnText);
     if (isNotEmpty(warnText))
         {
         success = FALSE;
         warn("%s", warnText);
         }
     if (regionCount == 0)
         {
         success = FALSE;
 	warn("No valid regions found in input; see below for formatting instructions");
         }
     cartSetString(cart, hgtaUserRegionsDb, database);
     cartSetString(cart, hgtaUserRegionsFile, trashFileName);
     cartSetString(cart, hgtaRegionType, hgtaRegionTypeUserRegions);
     if (strlen(idText) > 64 * 1024)
          cartRemove(cart, hgtaEnteredUserRegions);
     cartRemove(cart, hgtaEnteredUserRegionFile);
     }
 else
     {
     cartRemoveUserRegions();
     }
 if (success)
     mainPageAfterOpen(conn);
 else
     doSetUserRegionsAfterOpen(conn);
 htmlClose();
 }
 
 char *userRegionsFileName()
 /* File name defined regions are in, or NULL if no such file. */
 {
 char *fileName = cartOptionalString(cart, hgtaUserRegionsFile);
 char *db = cartOptionalString(cart, hgtaUserRegionsDb);
 if (db && !sameString(database, db))
     return NULL;
 if (fileName == NULL)
     return NULL;
+if (!isServerUserFilePath(fileName))
+    // Not a file we made, so don't read it and don't let the caller delete it.  Leave the
+    // cart alone as well -- cartRemoveUserRegions() here would throw away a region list
+    // that the user can still fix by re-entering it.
+    return NULL;
 if (fileExists(fileName))
     return fileName;
 else
     {
     cartRemoveUserRegions();
     return NULL;
     }
 }
 
 struct region *getUserRegions(char *fileName)
 /* Get user defined regions from fileName. */
 {
 struct region *list = NULL, *region;
 struct lineFile *lf;
 char *words[4];
 int wordCount;
 
 lf = lineFileOpen(fileName, TRUE); /* TRUE == replace CR with 0 */
 while (0 != (wordCount = lineFileChopNext(lf, words, ArraySize(words))))
     {
     AllocVar(region);
     region->chrom = cloneString(words[0]);
     region->start = atoi(words[1]);
     region->end = atoi(words[2]);
     if (wordCount > 3)
 	region->name = cloneString(words[3]);
     else
 	region->name = NULL;
     slAddHead(&list, region);
     }
 slReverse(&list);
 lineFileClose(&lf);
 return list;
 }
 
 void doClearSetUserRegionsText(struct sqlConnection *conn)
 /* Respond to clear within user regions enter page. */
 {
 char *fileName = userRegionsFileName();
 if (fileName != NULL)
     remove(fileName);
 cartRemoveUserRegions();
 doSetUserRegions(conn);
 }
 
 void doClearUserRegions(struct sqlConnection *conn)
 /* Respond to clear user regions button. */
 {
 char *fileName = userRegionsFileName();
 
 htmlOpen("Table Browser (Cleared Region List)");
 if (fileName != NULL)
     remove(fileName);
 cartRemoveUserRegions();
 mainPageAfterOpen(conn);
 htmlClose();
 }