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/trashDir.c src/hg/lib/trashDir.c
index 8c1fd4df789..817fa0749ec 100644
--- src/hg/lib/trashDir.c
+++ src/hg/lib/trashDir.c
@@ -1,26 +1,106 @@
 /* trashDir.c - temporary file creation and directory creation in /trash */
 
 /* Copyright (C) 2014 The Regents of the University of California 
  * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */
 
 
 #include "common.h"
 #include "hash.h"
+#include "hgConfig.h"
 #include "portable.h"
 #include "trashDir.h"
 
+static boolean hasDotDotComponent(char *path)
+/* Return TRUE if any '/'-separated component of path is exactly "..", which is the only
+ * way a path can climb back above a directory it appears to be inside of. */
+{
+char *s = path;
+while (s != NULL && s[0] != '\0')
+    {
+    if (s[0] == '.' && s[1] == '.' && (s[2] == '/' || s[2] == '\0'))
+        return TRUE;
+    s = strchr(s, '/');
+    if (s != NULL)
+        s += 1;
+    }
+return FALSE;
+}
+
+static boolean pathIsUnderDir(char *dir, char *path)
+/* Return TRUE if path names something underneath dir.  A '/' is required at the directory
+ * boundary, so a sibling directory whose name merely starts the same way (trashBackup next
+ * to trash) does not match.  ".." below the boundary is refused. */
+{
+if (isEmpty(dir) || isEmpty(path))
+    return FALSE;
+int dirLen = strlen(dir);
+while (dirLen > 0 && dir[dirLen-1] == '/')
+    dirLen -= 1;
+if (dirLen == 0)
+    return FALSE;
+if (strncmp(path, dir, dirLen) != 0 || path[dirLen] != '/' || path[dirLen+1] == '\0')
+    return FALSE;
+return !hasDotDotComponent(path + dirLen + 1);
+}
+
+boolean isTrashPath(char *path)
+/* Return TRUE if path names a file inside the trash directory. */
+{
+return pathIsUnderDir(trashDir(), path);
+}
+
+boolean isTrashOrSessionDataPath(char *path)
+/* Return TRUE if path is under the trash directory, or under one of the durable session-data
+ * directories that trash files are moved to when a session is saved. */
+{
+return isTrashPath(path) ||
+       pathIsUnderDir(cfgOption("sessionDataDir"), path) ||
+       pathIsUnderDir(cfgOption("sessionDataDirOld"), path);
+}
+
+boolean isServerUserFilePath(char *path)
+/* Return TRUE if path is under one of the directories where the server keeps files it made
+ * for a user: the trash directory, the session-data directories, or a per-feature data
+ * directory such as myVariantsDataDir. */
+{
+return isTrashOrSessionDataPath(path) ||
+       pathIsUnderDir(cfgOption("myVariantsDataDir"), path);
+}
+
+boolean isRemoteUrl(char *path)
+/* Return TRUE if path is a URL fetched over the network rather than a file name.  Only the
+ * three protocols the tree actually fetches count; hasProtocol() in net.c is a test for
+ * "://" anywhere in the string, which is too loose to decide anything on. */
+{
+return startsWith("http://", path) ||
+       startsWith("https://", path) ||
+       startsWith("ftp://", path);
+}
+
+boolean isServerUserFileOrUrl(char *path)
+/* Return TRUE if path is either a remote URL or a file the server made for a user.
+ *
+ * A few cart variables legitimately hold either one: the user gives hgTracks a URL for the
+ * multi-region BED or pastes the BED itself, and hgSession loads settings from a URL.  The
+ * code then decides which it has by looking for a protocol, and treats anything else as a
+ * local file name, so "no protocol" has to mean "one of ours" or the local-file branch reads
+ * whatever the cart says. */
+{
+return isRemoteUrl(path) || isServerUserFilePath(path);
+}
+
 static void trashDirFileExt(struct tempName *tn, char *dirName, char *base, char *suffix, boolean addDate)
 /*	obtain a trash file name trash/dirName/base*.suffix */
 {
 static struct hash *dirHash = NULL;
 char prefix[128];
 char buffer[4096];
 
 if (! dirHash)
 	dirHash = newHash(0);
 
 /* already created this directory ? */
 if (! hashLookup(dirHash,dirName))
     {
     hashAddInt(dirHash, dirName, 1);	/* remember, been here, done that */
     mkdirTrashDirectory(dirName);