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);