3eedbb7636648978b0371f2c54378e75fd1ac78c
braney
  Fri Sep 11 12:20:14 2026 -0700
Skip a CGI or cookie pair with no =value in three more parsers, refs #38340

The loop that #38335 fixed in the query string parsers is copied in three more
places, and each one still looks for the '=' across the whole rest of the string
instead of inside the pair it is reading.  A pair with no value therefore runs
into the pair after it and takes its value, and the same pair at the end of the
string has no '=' left to find and aborts.

lib/cheapcgi.c parseCookies         one bad cookie aborts every CGI for that
browser, on every request, until the
reader clears the cookie by hand
hg/hgSession/backup.c               a session backup silently leaves out a
custom track
hg/utils/refreshNamedSessionCustomTracks
one bad session aborts the child, the
parent exits non-zero, and every session
after it goes unscanned, so the trash
cleaner removes their custom track files

All three are behind the hg.conf flag skipMalformedCgiPairs, off by default, and
registered as a release gate in hgConfCatalog.  The kent libraries do not read
hg.conf, so hgConfig.c hands the setting to cheapcgi the way cfgSetLogCgiVars
already hands it cgiSetMaxLogLen.  The query string parsers do not read the
flag; they were fixed unconditionally under #38335.

refreshNamedSessionCustomTracks rebuilds the session contents as it walks, so it
copies a malformed pair through untouched rather than stepping over it.  A
session carrying one comes back byte for byte the same.

Adds lib/tests/cgiCookieTest and hg/hgSession/tests/backupParseTest.  Both read
every case with the flag off and on, so they pin the old behavior as well as the
new one.  The nightly tool has no seam for a unit test; its loop sits inside a
function that runs its own query.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

diff --git src/hg/hgSession/tests/backupParseTest.c src/hg/hgSession/tests/backupParseTest.c
new file mode 100644
index 00000000000..9c9a8c7ed44
--- /dev/null
+++ src/hg/hgSession/tests/backupParseTest.c
@@ -0,0 +1,77 @@
+/* backupParseTest - check how the session backup page treats a malformed pair
+ * in a stored cart contents string. */
+
+/* Copyright (C) 2026 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 "cart.h"
+#include "cheapcgi.h"
+#include "errCatch.h"
+#include "hgConfig.h"
+
+struct downloadResults
+/* Only the first two fields of backup.c's struct, which is all slCount and a
+ * db read here need.  next must stay first. */
+    {
+    struct downloadResults *next;
+    char *db;
+    };
+
+struct downloadResults *processCtsForDownloadInternals(char *contents, char **pTrackHubsVar);
+
+/* Three symbols backup.c wants from hgSession.c, which this test does not link. */
+struct cart *cart = NULL;
+char *database = NULL;
+
+char *cgiDecodeClone(char *encStr)
+/* Same as hgSession.c's. */
+{
+char *s = cloneString(encStr);
+cgiDecode(encStr, s, strlen(encStr));
+return s;
+}
+
+static char *cases[] = {
+/* one custom track, found */
+"db=hg38&ctfile_hg38=../trash/ct/probe.bed",
+/* an empty pair in front of it names it "&ctfile_hg38", which does not match
+ * the ctfile_ prefix, so the custom track is left out of the backup with no
+ * warning.  The reader gets a backup that is missing a track.  refs #38185 */
+"db=hg38&&ctfile_hg38=../trash/ct/probe.bed",
+/* a pair with no =value in front of it does the same.  refs #38340 */
+"db=hg38&i&ctfile_hg38=../trash/ct/probe.bed",
+/* either one after it aborts the whole backup instead */
+"db=hg38&ctfile_hg38=../trash/ct/probe.bed&g-catV2",
+"db=hg38&ctfile_hg38=../trash/ct/probe.bed&&",
+/* two assemblies, both found */
+"db=hg38&ctfile_hg38=../trash/ct/a.bed&ctfile_mm39=../trash/ct/b.bed",
+};
+
+int main(int argc, char *argv[])
+{
+int i;
+printf("hg.conf skipMalformedCgiPairs=%s\n\n",
+       cfgOptionBooleanDefault("skipMalformedCgiPairs", FALSE) ? "on" : "off");
+for (i = 0;  i < ArraySize(cases);  ++i)
+    {
+    printf("in  : %s\n", cases[i]);
+    printf("found: ");
+    struct errCatch *errCatch = errCatchNew();
+    if (errCatchStart(errCatch))
+	{
+	struct downloadResults *list, *result;
+	list = processCtsForDownloadInternals(cloneString(cases[i]), NULL);
+	printf("%d", slCount(list));
+	for (result = list;  result != NULL;  result = result->next)
+	    printf(" [%s]", result->db);
+	}
+    errCatchEnd(errCatch);
+    if (errCatch->gotError)
+	printf("aborted: %s", trimSpaces(errCatch->message->string));
+    errCatchFree(&errCatch);
+    printf("\n\n");
+    }
+return 0;
+}