MariaDB · Report #3891319
Stack buffer overflow in 9 of 10 Connector/ODBC catalog functions via unbounded name
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.
- 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 -lodbc3. 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 returnSQL_SUCCESSand the process exits 0. 4. Run it again with the last argument changed to 1000.SQLTablesreturnsHY090andSQLColumnskills 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 frommasterwithcmake .. -DCMAKE_C_FLAGS="-fsanitize=address -g" -DCMAKE_CXX_FLAGS="-fsanitize=address -g" -DWITH_SSL=OPENSSL -DWITH_EXTERNAL_ZLIB=OFF -DWITH_UNIT_TESTS=OFF && makeand point the connection string at the resultinglibmaodbc.so. 6. To see the saved return address take the caller's bytes, build the driver frommasterwith default flags and run./repro_f1 "<connstring>" 1400 aunder gdb. 7. Fixed control: apply the one-line clamp at the four escape sites inma_catalog.cpp, rebuild, and repeat steps 4 through 6. All of them returnSQL_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.