c2a6ef817930149ad6f55818fa0196d99c47e02b braney Fri Sep 11 10:40:11 2026 -0700 cheapcgi: skip a CGI pair with no =value instead of aborting, refs #38335 Both query string parsers looked for the '=' across the whole rest of the string rather than inside the current pair. A pair with no '=' in it therefore ran into the pair after it and took its value. "g-catV2&db=hg38" was stored as one variable named "g-catV2&db", so db was lost with no warning, and that corrupt name was copied on into the cart. The same pair at the end of the string had no '=' left to find and aborted the whole request, which is what the "Mangled CGI input string g-catV2" entries in the hgw1 logs were. Both parsers now find the end of the pair first, keeping the existing separator precedence ('&', then ';' for DAS), and skip a pair with no '='. A mixed "a=1;b=2&c=3" still parses the way it did. Adds lib/tests/cgiParseTest, which runs 18 query strings through both parsers. It covers the empty pair of #38185 as well. Co-Authored-By: Claude Opus 5 (1M context) diff --git src/lib/cheapcgi.c src/lib/cheapcgi.c index eb8f48a7e38..b502e3c32cf 100644 --- src/lib/cheapcgi.c +++ src/lib/cheapcgi.c @@ -987,62 +987,77 @@ * Returns a pointer to the terminating zero when nothing is left. * * Without this the parsers below, which split on the first separator after a * value, read the leftover one as part of the next name: "a=1&&b=2" stores "b" * under "&b". Nothing looks that name up, so the variable is silently lost. * The same empty pair at the end of the string has no '=' after it and instead * aborts the CGI. cgiEncode escapes everything but alphanumerics, '.' and '_', * so no encoded name can begin with a separator and none is ever eaten here. * refs #38185 */ { if (s == NULL) return NULL; return s + strspn(s, "&;"); } +static char *endCurrentPair(char *pair) +/* Zero-terminate the var=val pair that starts at pair, and return the start of + * whatever follows it, or NULL if it was the last one. + * + * The parsers below used to look for the separator only after the '=', which + * made them read across the end of a pair that has no '=' in it at all. A + * query string of "g-catV2&db=hg38" was stored as one variable named + * "g-catV2&db", so db was lost with no warning, and the same pair at the end of + * the string had no '=' left to find and aborted the whole request. Finding + * the end of the pair first confines both parsers to one pair at a time. + * refs #38335 */ +{ +char *end = strchr(pair, '&'); +if (end == NULL) + end = strchr(pair, ';'); /* Accomodate DAS. */ +if (end == NULL) + return NULL; +*end = 0; +return end+1; +} + boolean cgiParseNext(char **pInput, char **retVar, char **retVal) /* Parse out next var/val in a var=val&var=val... cgi formatted string * This will insert zeroes and other things into string. * Usage: * char *pt = cgiStringStart; * char *var, *val * while (cgiParseNext(&pt, &var, &val)) * printf("%s\t%s\n", var, val); */ { -char *var = skipEmptyPairs(*pInput); +char *var, *val; +for (;;) + { + var = skipEmptyPairs(*pInput); if (var == NULL || var[0] == 0) return FALSE; -char *val = strchr(var, '='); -if (val == NULL) - errAbort("Mangled CGI input string %s", var); -*val++ = 0; -char *end = strchr(val, '&'); -if (end == NULL) - end = strchr(val, ';'); // For DAS -if (end == NULL) - { - end = val + strlen(val); - *pInput = NULL; - } -else - { - *pInput = end+1; - *end = 0; + *pInput = endCurrentPair(var); + val = strchr(var, '='); + if (val != NULL) + break; + /* A pair with no '=' in it names nothing. Skip it rather than throwing + * away the rest of the request over it. refs #38335 */ } +*val++ = 0; *retVar = var; *retVal = val; -cgiDecode(val,val,end-val); +cgiDecode(val,val,strlen(val)); return TRUE; } void cgiSetMaxLogLen(int l) /* set the size of variable values that are dumped to stderr. Default is 0, which means no logging */ { logCgiVarMaxLen = l; } void cgiParseInputAbort(char *input, struct hash **retHash, struct cgiVar **retList) /* Parse cgi-style input into a hash table and list. This will alter * the input data. The hash table will contain references back * into input, so please don't free input until you're done with * the hash. Prints message aborts if there's an error. @@ -1051,41 +1066,40 @@ char *namePt, *dataPt, *nextNamePt; struct hash *hash = *retHash; struct cgiVar *list = *retList, *el; if (!hash) hash = newHash(6); slReverse(&list); struct dyString *logMsg = NULL; if (logCgiVarMaxLen > 0) logMsg = dyStringNew(1024); namePt = input; while ((namePt = skipEmptyPairs(namePt)) != NULL && namePt[0] != 0) { + nextNamePt = endCurrentPair(namePt); dataPt = strchr(namePt, '='); if (dataPt == NULL) { - errAbort("Mangled CGI input string %s", namePt); + /* A pair with no '=' in it names nothing. Skip it rather than + * aborting and throwing away the rest of the request. refs #38335 */ + namePt = nextNamePt; + continue; } *dataPt++ = 0; - nextNamePt = strchr(dataPt, '&'); - if (nextNamePt == NULL) - nextNamePt = strchr(dataPt, ';'); /* Accomodate DAS. */ - if (nextNamePt != NULL) - *nextNamePt++ = 0; if (logMsg && dataPt && strlen(dataPt) < logCgiVarMaxLen) dyStringPrintf(logMsg, "%s=%s ", namePt, dataPt); // if dataPt is empty string, still print it, could be important cgiDecode(namePt,namePt,strlen(namePt)); /* for unusual ct names */ cgiDecode(dataPt,dataPt,strlen(dataPt)); AllocVar(el); el->val = dataPt; slAddHead(&list, el); hashAddSaveName(hash, namePt, el, &el->name); namePt = nextNamePt; } if (logMsg)