a4befd9382fadf413884d2215012535d5d667063
braney
  Mon Aug 17 13:46:45 2026 -0700
hgApi, hgTracks: tighten callback parameter validation, refs #38126 #38057

Add isValidJsonpCallback() and apply it to the callback-name paths in
apiOut() and the hgTracks jsonp output, so only C-symbol dotted names are
echoed back.

diff --git src/lib/cheapcgi.c src/lib/cheapcgi.c
index 5e972091198..cb73dcd9463 100644
--- src/lib/cheapcgi.c
+++ src/lib/cheapcgi.c
@@ -2962,15 +2962,46 @@
 char *s = NULL;
 for (el = elList; el != NULL; el = el->next)
     {
     if (firstTime)
 	firstTime = FALSE;
     else
 	dyStringAppendC(dy, '&');
     dyStringAppend(dy, el->name);
     dyStringAppendC(dy, '=');
     s = cgiEncode(el->val);
     dyStringAppend(dy, s);
     freez(&s);
     }
 hashElFreeList(&elList);
 }
+
+boolean isValidJsonpCallback(char *s)
+/* Return TRUE if s is safe to use as a JSONP callback name: non-empty, not
+ * too long, and every dot-separated segment is a C symbol (letters, digits,
+ * underscore, not starting with a digit).  This rejects anything with
+ * parentheses, spaces, operators, or other characters that would let an
+ * attacker turn a same-origin JSONP response into arbitrary script. */
+{
+if (isEmpty(s))
+    return FALSE;
+if (strlen(s) > 128)
+    return FALSE;
+char *dupe = cloneString(s);
+boolean ok = TRUE;
+char *seg = dupe;
+char *dot;
+while (seg != NULL)
+    {
+    dot = strchr(seg, '.');
+    if (dot != NULL)
+	*dot = 0;
+    if (!isSymbolString(seg))
+	{
+	ok = FALSE;
+	break;
+	}
+    seg = (dot != NULL) ? dot + 1 : NULL;
+    }
+freeMem(dupe);
+return ok;
+}