67700f0c574a5b49df7e49eff54e16221ff7e6ea braney Tue Sep 8 15:38:12 2026 -0700 cart: accept a session file path spelled through a symlinked config directory The file-name check added in 9ad04e0a0b0 compares a path against trashDir(), sessionDataDir, sessionDataDirOld and myVariantsDataDir as plain strings. Those directories are often reached through a symlink, and sessionData.c stores the resolved spelling whenever the trash file it is saving is already a relative symlink, so a saved session can hold either spelling. On the RR /userdata is a symlink to /shared/userdata and 583 saved multi-region sessions hold the resolved form, so hgTracks dropped multiRegionsBedUrl and said "No BED or BED URL specified" rather than drawing the saved view. Accept a path under a trusted directory or under whatever that directory resolves to. Only the configured directory is resolved. Resolving the value itself is not an option, because a trash file is deliberately a symlink into session storage. Use a stack buffer rather than realpath(dir, NULL): that allocates with the system malloc, and freeMem() goes through the kent handler stack, which under pushCarefulMemHandler() reads a block header that is not there. hgc, hgTables, hgVai, hgLogin and hgLinkIn all install that handler before loading a cart. refs #38303, refs #37623 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> diff --git src/hg/lib/trashDir.c src/hg/lib/trashDir.c index 9a95956fbbb..8b1d9967939 100644 --- src/hg/lib/trashDir.c +++ src/hg/lib/trashDir.c @@ -1,195 +1,213 @@ /* 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 <limits.h> #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; } 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); } +static boolean pathIsUnderDirOrItsTarget(char *dir, char *path) +/* pathIsUnderDir(), but also accept a path under the directory that dir resolves to. A + * configured directory is often reached through a symlink, and sessionData.c stores the + * resolved spelling of a path whose file is already a symlink, so both spellings turn up in + * saved sessions. Only dir is resolved. Resolving path would defeat the check, because a + * trash file is often a symlink into session storage on purpose. */ +{ +if (pathIsUnderDir(dir, path)) + return TRUE; +if (isEmpty(dir)) + return FALSE; +char resolved[PATH_MAX]; +if (realpath(dir, resolved) == NULL) + return FALSE; +return pathIsUnderDir(resolved, path); +} + boolean isTrashPath(char *path) /* Return TRUE if path names a file inside the trash directory. */ { -return pathIsUnderDir(trashDir(), path); +return pathIsUnderDirOrItsTarget(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); + pathIsUnderDirOrItsTarget(cfgOption("sessionDataDir"), path) || + pathIsUnderDirOrItsTarget(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); + pathIsUnderDirOrItsTarget(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); } if (addDate) { safef(buffer, sizeof buffer, "%s/%03d", dirName, dayOfYear()); dirName = buffer; if (! hashLookup(dirHash,dirName)) { hashAddInt(dirHash, dirName, 1); /* remember, been here, done that */ mkdirTrashDirectory(dirName); } } /* no need to duplicate the _ at the end of base, makeTempName is going * to add _ to the given base, some CGIs pass "base_" */ if (endsWith(base,"_")) { char *t = cloneString(base); int len = strlen(t); t[len-1] = '\0'; /* remove ending _ */ safef(prefix, sizeof(prefix), "%s/%s", dirName,t); freeMem(t); } else safef(prefix, sizeof(prefix), "%s/%s", dirName,base); makeTempName(tn, prefix, suffix); } void trashDirFile(struct tempName *tn, char *dirName, char *base, char *suffix) /* obtain a trash file name trash/dirName/base*.suffix */ { trashDirFileExt(tn, dirName, base, suffix, FALSE); } void trashDirDateFile(struct tempName *tn, char *dirName, char *base, char *suffix) /* obtain a trash file name trash/dirName.dayOfYear/base*.suffix */ { trashDirFileExt(tn, dirName, base, suffix, TRUE); } boolean trashDirReusableFile(struct tempName *tn, char *dirName, char *base, char *suffix) /* obtain a resusable trash file name as trash/dirName/base.suffix * returns TRUE if already exists. */ { trashDirFile(tn,dirName,base,suffix); // Don't really want the randomized name. char *cgiName = rStringIn("/",tn->forCgi ); char *htmlName = rStringIn("/",tn->forHtml); if (cgiName == NULL) cgiName = rStringIn("\\",tn->forCgi); assert(cgiName != NULL && htmlName != NULL); cgiName += 1; htmlName += 1; boolean addDot = (*suffix != '.'); safef(cgiName, strlen(cgiName), "%s%s%s", base, (addDot?".":""),suffix);// There is room, since safef(htmlName,strlen(htmlName),"%s%s%s", base, (addDot?".":""),suffix);// tempName: base_*.suffix // exists? return fileExists(tn->forCgi); } void copyFileToTrash(char **pFileName, char *dirName, char *base, char *suffix) /* If *pFileName is not NULL and exists, then create a new file in the * given dirName of trash/ with the given base and suffix, copy *pFileName's * contents to it, and set *pFileName to the new filename. */ { if (pFileName != NULL && *pFileName != NULL) { if (fileExists(*pFileName)) { FILE *fIn = mustOpen(*pFileName, "r"); struct tempName tn; trashDirFile(&tn, dirName, base, suffix); char *newFileName = tn.forCgi; FILE *fOut = mustOpen(newFileName, "w"); unsigned char buf[16 * 1024]; size_t sz; while ((sz = fread(buf, sizeof(buf[0]), ArraySize(buf), fIn)) > 0) fwrite(buf, sizeof(buf[0]), sz, fOut); fclose(fOut); fclose(fIn); *pFileName = cloneString(newFileName); } } }