9b6210bc4e86c38aff5a505b3c40cb90e3a4b9b8
hiram
  Fri Jul 31 13:37:13 2026 -0700
pass relay secret string in http headers instead of in the URL and fixup a bare SQL injection refs #31811

diff --git src/hg/hubApi/apiUtils.c src/hg/hubApi/apiUtils.c
index a1c68258f60..9ddde8a37df 100644
--- src/hg/hubApi/apiUtils.c
+++ src/hg/hubApi/apiUtils.c
@@ -923,31 +923,33 @@
         else
             {
             /* Atomic insert with duplicate re-check, closing the race window
              * between the SELECT above and this INSERT. */
             struct dyString *update = dyStringNew(0);
             sqlDyStringPrintf(update,
                 "INSERT INTO %s (requestType, fromDb, toDb, email, comment, requestTime, status, buildDir) "
                 "SELECT 'liftOver', '%s', '%s', '%s', '%s', now(), 0, '' "
                 "WHERE NOT EXISTS ("
                 "  SELECT 1 FROM %s WHERE requestType='liftOver' AND "
                 "  ((fromDb='%s' AND toDb='%s') OR (fromDb='%s' AND toDb='%s'))"
                 ")",
                 ottoTable, fromDb, toDb, email, comment,
                 ottoTable, fromDb, toDb, toDb, fromDb);
             sqlUpdate(conn, dyStringCannibalize(&update));
-            int rowsAffected = sqlQuickNum(conn, "SELECT ROW_COUNT()");
+            char rowCountQuery[64];
+            sqlSafef(rowCountQuery, sizeof(rowCountQuery), "SELECT ROW_COUNT()");
+            int rowsAffected = sqlQuickNum(conn, rowCountQuery);
             status = (rowsAffected > 0) ? "accepted" : "duplicate";
             }
         }
     }
 else if (sameString(requestType, "assembly"))
     {
     struct dyString *update = dyStringNew(0);
     sqlDyStringPrintf(update,
         "INSERT INTO %s (requestType, fromDb, toDb, email, comment, requestTime, status, buildDir) "
         "VALUES ('assembly', '%s', '%s', '%s', '%s', now(), 0, '')",
         ottoTable, fromDb, toDb, email, comment);
     sqlUpdate(conn, dyStringCannibalize(&update));
     status = "accepted";
     }
 hDisconnectCentral(&conn);
@@ -958,39 +960,41 @@
 /* Relay an ottoRequest submission to genome.ucsc.edu's /submitOttoRequest
  * endpoint, for hosts that lack local hgcentral write grants (see
  * inUcscEduDomain()/onGenomeRRMachine()).  Authenticates with the shared
  * hg.conf secret 'hubApi.relaySecret', which must also be configured on
  * genome.ucsc.edu.  Returns the same status vocabulary as
  * submitOttoRequest(): "disabled", "duplicate", "rateLimited", "accepted",
  * or "error" (including when the secret isn't configured locally, or the
  * relay call itself fails). */
 {
 char *secret = cfgOption("hubApi.relaySecret");
 if (isEmpty(secret))
     return "error";
 
 struct dyString *url = dyStringNew(0);
 dyStringPrintf(url, "https://genome.ucsc.edu/cgi-bin/hubApi/submitOttoRequest"
-    "?%s=%s&%s=%s&%s=%s&%s=%s&%s=%s&%s=%s",
+    "?%s=%s&%s=%s&%s=%s&%s=%s&%s=%s",
     argRequestType, cgiEncode(requestType),
     argFromGenome, cgiEncode(fromDb),
     argToGenome, cgiEncode(toDb),
     argEmail, cgiEncode(email),
-    argComment, cgiEncode(comment),
-    argRelaySecret, cgiEncode(secret));
+    argComment, cgiEncode(comment));
 
-int sd = netUrlOpen(dyStringContents(url));
+struct dyString *header = dyStringNew(0);
+dyStringPrintf(header, "X-Relay-Secret: %s\r\n", secret);
+int sd = netOpenHttpExt(dyStringContents(url), "GET", dyStringContents(header));
+dyStringFree(&header);
 if (sd < 0)
     {
     dyStringFree(&url);
     return "error";
     }
 char *redirectedUrl = NULL;
 if (!netSkipHttpHeaderLinesWithRedirect(sd, dyStringContents(url), &redirectedUrl))
     {
     close(sd);
     dyStringFree(&url);
     return "error";
     }
 dyStringFree(&url);
 struct dyString *body = netSlurpFile(sd);
 close(sd);
@@ -1002,31 +1006,31 @@
 return isNotEmpty(status) ? cloneString(status) : "error";
 }
 
 void apiSubmitOttoRequest(char *words[MAX_PATH_INFO])
 /* Internal server-to-server endpoint: record a row in the ottoRequest table
  * on behalf of a hubApi host that lacks its own hgcentral write grants (see
  * relaySubmitOttoRequest()).  Requires the shared hg.conf secret
  * 'hubApi.relaySecret' to match, since this performs the actual database
  * write without the bot-challenge cookie check that /liftRequest and
  * /assemblyRequest do locally -- that check already ran on the relaying
  * host before it got here.  Always answers 200 + a JSON "status" field;
  * never apiErrAbort()s for a duplicate/rateLimited outcome, so the relay
  * caller can distinguish it from a hard failure. */
 {
 char *secret = cfgOption("hubApi.relaySecret");
-char *givenSecret = cgiOptionalString(argRelaySecret);
+char *givenSecret = getenv("HTTP_X_RELAY_SECRET");
 if (isEmpty(secret) || isEmpty(givenSecret) || !sameString(secret, givenSecret))
     apiErrAbort(err403, err403Msg, "not authorized for endpoint '/submitOttoRequest");
 
 char *requestType = cgiOptionalString(argRequestType);
 char *fromDb = cgiOptionalString(argFromGenome);
 char *toDb = cgiOptionalString(argToGenome);
 char *email = cgiOptionalString(argEmail);
 char *comment = cgiOptionalString(argComment);
 if (isEmpty(requestType) || isEmpty(fromDb) || isEmpty(toDb) || isEmpty(email) || isEmpty(comment))
     apiErrAbort(err400, err400Msg, "must have all arguments: %s, %s, %s, %s, %s for endpoint '/submitOttoRequest",
         argRequestType, argFromGenome, argToGenome, argEmail, argComment);
 if (!sameString(requestType, "liftOver") && !sameString(requestType, "assembly"))
     apiErrAbort(err400, err400Msg, "unrecognized %s '%s' for endpoint '/submitOttoRequest", argRequestType, requestType);
 
 char *status = submitOttoRequest(requestType, fromDb, toDb, email, comment);