HexBrain

MariaDB · Report #3891319

Stack buffer overflow in 9 of 10 Connector/ODBC catalog functions via unbounded name

Validstatus
mediumseverity
3upvotes
4 Nov 2026disclosed

How it was found

Nine of the ten catalog functions in MariaDB Connector/ODBC escape an application-supplied identifier into a fixed 513-byte stack buffer without checking its length. Any name longer than 256 bytes overflows that buffer with attacker-controlled bytes. SQLColumns, SQLPrimaryKeys, SQLStatistics, SQLSpecialColumns, SQLProcedures, SQLProcedureColumns, SQLColumnPrivileges, SQLTablePrivileges and SQLForeignKeys are affected. SQLTables is the only one that validates the length, and it rejects the identical input with HY090, which is what makes this an inconsistency rather than a design decision.

Reproduced on the released odbc-mariadb 3.2.6-1.1 package as shipped by Debian and Ubuntu, and on current master HEAD 4e175d9 (3.2.10).

Details

AddPvCondition() and AddOaCondition() build the WHERE conditions of the driver-generated catalog queries. Both exist in a stream variant and a buffer variant, and all four declare the same destination buffer, at driver/ma_catalog.cpp:35, :53, :108 and :131:

static int AddPvCondition(MADB_Dbc *dbc, void* buffer, size_t bufferLen, char* value, SQLSMALLINT len)
{
  char escaped[2*NAME_LEN + 1];
  /* We should probably compare to SQL_NTS, and throw error if just < 0, but I think this way it will work just fine */
  if (len < 0)
  {
    len= (SQLSMALLINT)strlen(value);
  }

  len= (SQLSMALLINT)mysql_real_escape_string(dbc->mariadb, escaped, value, len);

NAME_LEN is 256, so escaped is 513 bytes. That size is only correct if len is bounded by NAME_LEN, because mysql_real_escape_string() writes up to 2 * len + 1 bytes. Nothing bounds len. It arrives from the application through AddPvOrIdCondition() and AddOaOrIdCondition() (:192, :227), which pass the caller's NameLength through untouched whenever SQL_ATTR_METADATA_ID holds its default value of SQL_FALSE.

The path from the public API is direct. MADB_StmtColumns() hands the application's TableName and NameLength3 to AddPvOrIdCondition() at driver/ma_catalog.cpp:666, and MA_SQLColumns() reaches it from SQLColumns with no validation in between. A 1000-byte name produces a 2001-byte write into a 513-byte stack buffer. The overflow is contiguous, runs off the end of the enclosing frame, and its content is the escaped form of the caller's string.

The intent is documented by the codebase itself. MADB_StmtTables() performs exactly this check before doing any of the work, at driver/ma_catalog.cpp:376-378:

  /* TODO: Here we need compare character length in fact. Comparing with both either NAME_CHAR_LEN or NAME_LEN don't make quite sense, but
     if > NAME_LEN, then it is for sure too big */
  if (CatalogNameLength > NAME_LEN || TableNameLength > NAME_LEN)

That check exists in one of the ten catalog functions. The other nine call the same helpers with the same kind of argument and never perform it.

Passing SQL_NTS, which is what most applications do rather than measuring the string themselves, does not help: the len < 0 branch recomputes the length with strlen() and the same overflow follows.

Observed behaviour on the stock odbc-mariadb 3.2.6-1.1 shared object, varying only the identifier length:

  namelen=250  exit=0
  namelen=256  exit=0
  namelen=257  exit=0
  namelen=260  exit=0
  namelen=300  exit=134   *** stack smashing detected ***: terminated
  namelen=400  exit=139   SIGSEGV

With the driver built from master under AddressSanitizer, the write is attributed precisely:

==15368==ERROR: AddressSanitizer: stack-buffer-overflow
WRITE of size 1
    #0 mysql_cset_escape_slashes libmariadb/ma_charset.c:1212
    #1 mysql_real_escape_string libmariadb/mariadb_lib.c:4338
    #2 AddPvCondition driver/ma_catalog.cpp:60
    #3 AddPvOrIdCondition driver/ma_catalog.cpp:204
    #4 MADB_StmtColumns driver/ma_catalog.cpp:666
    #5 MA_SQLColumns driver/ma_api_internal.cpp:584
    #6 SQLColumns driver/odbc_3_api.cpp:299

Address is located in stack of thread T0 at offset 561 in frame AddPvCondition
  This frame has 1 object(s):
    [48, 561) 'escaped' (line 53) <== Memory access at offset 561 overflows this variable

The same 1000-byte name given to SQLTables is rejected cleanly on the same driver:

    SQLSTATE=HY090 : [ma-3.2.6][11.4.12-MariaDB-ubu2404]Table and catalog names are limited to 64 chars

How far this goes depends on how the driver was compiled, and the two cases differ enough to state separately.

The Debian and Ubuntu package is built with -fstack-protector-strong. nm -D on it shows a __stack_chk_fail reference, and at 300 bytes the canary catches the smash and the process aborts with *** stack smashing detected ***. At 400 bytes and above it segfaults before reaching the canary check.

The project's own CMakeLists.txt does not add -fstack-protector, so a default build from upstream source produces a shared object with no __stack_chk_fail symbol at all. On that build, filling the name with a chosen byte places that byte on the saved return address. With 1400 bytes of a passed to SQLPrimaryKeys:

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7cb585d in AddOaCondition (...) at driver/ma_catalog.cpp:161
161	}
#0  0x00007ffff7cb585d in AddOaCondition (...) at driver/ma_catalog.cpp:161
#1  0x6161616161616161 in ?? ()
#2  0x6161616161616161 in ?? ()
#3  0x6161616161616161 in ?? ()

The fault is at the closing brace of AddOaCondition, that is at its ret. Filling the name with ' instead yields 0x275c275c275c275c, the escaped form \' repeated, and passes an attacker-controlled pointer to realloc().

Fixed control: adding the bound that MADB_StmtTables already applies, at the four escape sites, makes every input above return SQL_SUCCESS and exit cleanly.

  if (len > NAME_LEN) len= NAME_LEN;
  len= (SQLSMALLINT)mysql_real_escape_string(dbc->mariadb, escaped, value, len);

PoC

repro_f1_catalog_overflow.c in the attachment is a single self-contained file. It runs SQLTables and SQLColumns with the same identifier so the difference between the guarded and the unguarded function is visible in one run. No custom driver build is needed; the distribution package reproduces it.

  1. Install the packaged driver, for example apt install odbc-mariadb unixodbc unixodbc-dev, and start any MariaDB server. 2. Build the reproducer: gcc -O0 -g -o repro_f1 repro_f1_catalog_overflow.c -lodbc 3. Run the control with a 200-byte identifier: ./repro_f1 "DRIVER=/usr/lib/x86_64-linux-gnu/odbc/libmaodbc.so;SERVER=127.0.0.1;PORT=3306;UID=user;PWD=pass;DATABASE=test;SSLVERIFY=0;" 200. Both calls return SQL_SUCCESS and the process exits 0. 4. Run it again with the last argument changed to 1000. SQLTables returns HY090 and SQLColumns kills the process, either with *** stack smashing detected *** or with SIGSEGV depending on how the driver was compiled. 5. To see the write attributed to source, rebuild the driver from master with cmake .. -DCMAKE_C_FLAGS="-fsanitize=address -g" -DCMAKE_CXX_FLAGS="-fsanitize=address -g" -DWITH_SSL=OPENSSL -DWITH_EXTERNAL_ZLIB=OFF -DWITH_UNIT_TESTS=OFF && make and point the connection string at the resulting libmaodbc.so. 6. To see the saved return address take the caller's bytes, build the driver from master with default flags and run ./repro_f1 "<connstring>" 1400 a under gdb. 7. Fixed control: apply the one-line clamp at the four escape sites in ma_catalog.cpp, rebuild, and repeat steps 4 through 6. All of them return SQL_SUCCESS.

CVSS 3.1: AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H = 8.1 for a build without a stack canary, which is what the project's own CMakeLists produces. For the hardened distribution builds, where the canary converts the overflow into a controlled abort, AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H = 5.9 is the accurate reading. AC:H in both reflects that the attacker needs a calling application that routes untrusted input into a catalog function.

Suggested fix: reject or clamp identifiers longer than NAME_LEN before escaping them, the same way MADB_StmtTables already does. Adding -fstack-protector-strong to the project's own build flags would be worthwhile independently.

Impact

Memory corruption in the process that loads the driver, triggered by the length of an identifier the application passes to a catalog function.

On the hardened distribution builds the demonstrated result is a reliable, remotely reachable abort or segfault, so any application that can be induced to pass a long identifier can be killed. On a driver built from upstream source with the project's own default flags there is no stack canary, and the saved return address holds bytes the caller chose, which is the precondition for control-flow hijack rather than a crash alone.

Exposure depends on where the identifier comes from. An application that only ever passes literal table names is not reachable. An application that forwards a user-supplied name or search pattern into a catalog lookup is, and that is the documented purpose of these arguments: TableName and ColumnName on SQLColumns, and the equivalents on SQLProcedures, SQLProcedureColumns, SQLColumnPrivileges and SQLTablePrivileges, are pattern value arguments in the ODBC specification, which is to say search patterns. Schema browsers, reporting and BI front ends, data catalogues and admin consoles are the realistic shape of an affected caller.

No database privileges beyond those the application already holds are required, and no server cooperation is needed. The overflow happens while the driver is assembling the query string, before anything is sent to the server.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original