af0aeebb6fbccc3364ce3c85e7052baa2b124f10
braney
  Tue Aug 11 09:22:17 2026 -0700
hg/lib/tests: add a sqlCheck case for the 0x01 escape marker bypass, refs #38051

New XM type wraps the given value in the 0x01 marker that sqlSafef uses
internally to delimit the spans it must escape, then calls sqlSafef. Two markers
forge an extra delimiter pair and used to leave the text between them unescaped,
so this must now abort.

The payload is built inside sqlCheck.c rather than passed in from the makefile,
so the test does not need a control byte in a shell argument.

This case passes whether or not HGDB_CONF is set, because the guard it exercises
uses a hard errAbort rather than sqlCheckError. Note that sqlCheck is not in the
default test target, and that case 3 does need HGDB_CONF set in order to abort.

diff --git src/hg/lib/tests/sqlCheck.c src/hg/lib/tests/sqlCheck.c
index a18bebf82ac..eba91ef682a 100644
--- src/hg/lib/tests/sqlCheck.c
+++ src/hg/lib/tests/sqlCheck.c
@@ -12,30 +12,31 @@
 
 void usage()
 /* display usage message */
 {
 printf(
  "sqlCheck - test anti-sql-injection functions\n"
  "\n"
  "Usage:\n"
  " sqlCheck type value\n"
  "\n"
  "where type can be \n"
  "  ID for Identifier -- these should be alphanumeric, underscore, and period but not allow spaces, quotes, etc\n"
  "  IL for Identifier List -- these should be identifiers comma-separated list. Currently '.' is allowed as a special exception.\n"
  "  ES for Escape the string using mysql -- this should allow all and escape all characters except 0 (which is useful for binary but not strings)\n"
  "  EE for Escape Every evil character -- this should append escaped all forbidden characters except 0 (which is useful for binary but not strings)\n"
+ "  XM for escape Marker bypass -- wraps the value in the 0x01 escape marker sqlSafef uses internally; this must abort, refs #38051\n"
  "\n"
  );
 exit(1);
 }
 
 int main(int argc, char *argv[])
 {
 if (argc != 3)
     usage();
 
 // since we have not gotten a db connection yet,
 // apparently we have to call this to avoid a segfault.
 mysql_library_init(0, 0, 0);
 
 char *theType = argv[1];
@@ -66,30 +67,41 @@
     sqlDyAppendEscaped(dy, "\x1a\n\r\\\'\"");  // typically used when there are unusual characters that need escaping.
     sqlDyStringPrintf(dy, "');");
     printf("%s\n", dy->string);
     }
 else if (sameString(theType,"XX")) // test sqlSafef
     {
     char query[1024];   // quoted %-s is not allowed anymore since it was poorly defined and useless.
     //sqlSafef(query, sizeof query, "SELECT * FROM %s where field = '%s'", value, "value");
     sqlSafef(query, sizeof query, "SELECT * FROM %s where field = '%s'", "table", value);
     //sqlSafef(query, sizeof query, "SELECT * FROM %s where id=%d and field like '%%%s'", "table", 3, value);
     //sqlSafef(query, sizeof query, "SELECT * FROM %s where id=%d and field like '%%%s' and ptr=%p", "table", 3, value, value);
     //sqlSafef(query, sizeof query, "SELECT * FROM %s where field = '%-s'", "table", value);
     //sqlSafef(query, sizeof query, "SELECT %-s FROM TABLE where field = '%s'", sqlCkIl(value), "value");
     printf("query=%s\n", query);
     }
+else if (sameString(theType,"XM")) // test sqlSafef against the escape-marker bypass, refs #38051
+    {
+    // Wrap the value in the 0x01 marker that sqlSafef uses internally to delimit
+    // the spans it must escape.  Two of them forge an extra delimiter pair, and
+    // sqlEscapeAllStrings then copies the text between them raw.  This must abort.
+    char evil[512];
+    safef(evil, sizeof evil, "x%c%s%cx", 0x01, value, 0x01);
+    char query[1024];
+    sqlSafef(query, sizeof query, "SELECT * FROM %s where field = '%s'", "table", evil);
+    printf("query=%s\n", query);
+    }
 else if (sameString(theType,"XY")) // test sqlDyStringPrintf
     {
     struct dyString *dy = dyStringNew(200);  // no quoted %-s anymore, deemed useless and forbidden.
     //sqlDyStringPrintf(dy, "SELECT * FROM %s where field = '%s'", value, "value");
     sqlDyStringPrintf(dy, "SELECT * FROM %s where field = '%s'", "table", value);
     //sqlDyStringPrintf(dy, " AND field2 = '%s'", value);  // make sure appending works without duplicating the NOSQLINJ prefix
     //sqlDyStringPrintf(dy, "SELECT * FROM %s where id=%d and field like '%%%s'", "table", 3, value);
     //sqlDyStringPrintf(dy, "SELECT * FROM %s where id=%d and field like '%%%s' and ptr=%p", "table", 3, value, value);
     //sqlDyStringPrintf(dy, "SELECT * FROM %s where field = '%-s'", "table", value);
     printf("dy=%s\n", dy->string);
     }
 else
     {
     usage();
     }