HexBrain

MariaDB · Report #3871977

Connector/ODBC: malicious server's long version string wraps size_t PrefixLen via negative _snprintf return, causing heap out-of-bounds write

Validstatus
mediumseverity
7upvotes
12 Oct 2026disclosed

How it was found

MariaDB Connector/ODBC formats every diagnostic message with a prefix that embeds the server version string. The prefix is built in MADB_PutErrorPrefix (driver/ma_error.cpp:165) with the driver's custom _snprintf, which returns a NEGATIVE value when the output is truncated (driver/ma_platform_posix.cpp:41). The return value is added to error->PrefixLen, a size_t (unsigned, driver/ma_c_stuff.h:70). A malicious or MITM MariaDB server that sends a handshake server_version string longer than ~500 bytes causes _snprintf to truncate and return negative, wrapping PrefixLen to ~2^64. Every subsequent use of PrefixLen as an offset into the fixed 513-byte SqlErrorMsg buffer then writes out of bounds at a server-chosen backward offset.

This is an unauthenticated, remotely triggered client-side memory corruption that fires as soon as the application allocates a statement on the poisoned connection. Confirmed end-to-end with ASAN against the real driver built from HEAD 4e175d9 (v3.2.10).

Root cause

  1. Custom _snprintf returns negative on truncation (driver/ma_platform_posix.cpp:41):
int _snprintf(char *buffer, size_t count, const char *format, ...) {
    int result = vsnprintf(buffer, count, format, list);
    if (result > count) { return count - result - 1; }   // NEGATIVE
    return result;
}
  1. MADB_PutErrorPrefix adds the negative return to a size_t (driver/ma_error.cpp:155-170):
error->PrefixLen = strlen(MARIADB_ODBC_ERR_PREFIX);   // "[ma-3.2.10]" ~ 11
strcpy_s(error->SqlErrorMsg, SQL_MAX_MESSAGE_LENGTH + 1, MARIADB_ODBC_ERR_PREFIX);
if (dbc != NULL && dbc->mariadb != NULL) {
  error->PrefixLen += _snprintf(error->SqlErrorMsg + error->PrefixLen,
    SQL_MAX_MESSAGE_LENGTH + 1 - error->PrefixLen,
    "[%s]", mysql_get_server_info(dbc->mariadb));
    // ^^ attacker-controlled, uncapped version string from handshake
}

With a version string of length L > ~500, the _snprintf return is negative, and PrefixLen += <negative> wraps to approximately 2^64 - (L - 510). The backward distance is chosen by the attacker via the version-string length.

  1. Two OOB write sinks use the corrupted PrefixLen:
  • MADB_CLEAR_ERROR / MA_ClearError (driver/ma_error.h:168, driver/ma_api_internal.cpp:58): SqlErrorMsg[PrefixLen] = 0; - one-byte OOB write
  • MADB_SetNativeError (driver/ma_error.cpp:231,235): strcpy_s(SqlErrorMsg + PrefixLen, 513 - PrefixLen, msg) - multi-byte OOB write with server-controlled error text

Steps To Reproduce

Environment: Linux x86-64, unixODBC 2.3.14, driver built from HEAD 4e175d9 with AddressSanitizer (cmake -DWITH_ASAN=ON).

Step 0 - Confirm libmariadb exposes the long version (direct_version_check.c attached):

$ ./direct_version_check 3402   # rogue server version_len=2000
server_info_len=2000  first16=AAAAAAAAAAAAAAAA

mysql_get_server_info() returns the full 2000-byte handshake version uncapped.

Step 1 - Start the rogue server (rogue_server.py attached):

python3 rogue_server.py 3404 4000 &   # port 3404, version string = 4000 bytes

The rogue server speaks enough MariaDB wire protocol for the connection to succeed and advertises a 4000-byte server_version in the handshake.

Step 2 - POSITIVE (attack) - real ASAN-built driver + real unixODBC (odbc_client.c attached):

./odbc_client "DRIVER=.../build_asan/driver/libmaodbc.so;SERVER=127.0.0.1;PORT=3404;UID=u;PWD=p;SSLVERIFY=0;"

Output:

SQLDriverConnect r=0                        <- connection SUCCEEDS
allocating statement (MADB_StmtInit -> PutErrorPrefix)
==ERROR: AddressSanitizer: heap-buffer-overflow ... WRITE of size 1
    #0 MA_ClearError(short, void*)  driver/ma_api_internal.cpp:58
    #1 SQLGetStmtAttr               driver/odbc_3_api.cpp:997
    #2 libodbc.so.2                 (unixODBC Driver Manager)
    #3 main                         odbc_client.c:32

The OOB write fires at statement allocation. No application query needed.

Step 3 - NEGATIVE CONTROL - identical path, normal version string (20 bytes):

python3 rogue_server.py 3405 20 &
./odbc_client "DRIVER=...;SERVER=127.0.0.1;PORT=3405;UID=u;PWD=p;SSLVERIFY=0;"

Output:

SQLDriverConnect r=0
SQLAllocHandle(STMT) r=0
SQLExecDirect r=0 (survived)
clean exit    EXIT=0

Normal-length version string causes no corruption. The long version is the sole difference.

Automated reproduction: bash run_poc.sh (attached) builds the driver with ASAN, starts the rogue server, runs both positive and negative cases.

Supporting Material/References

  • Vulnerable function: MADB_PutErrorPrefix at driver/ma_error.cpp:165
  • Negative-return source: _snprintf at driver/ma_platform_posix.cpp:41
  • PrefixLen type: size_t at driver/ma_c_stuff.h:70
  • OOB sink 1: MA_ClearError at driver/ma_api_internal.cpp:58
  • OOB sink 2: MADB_SetNativeError at driver/ma_error.cpp:231
  • Trigger path: MADB_StmtInit at driver/ma_statement.cpp:4113
  • The same _snprintf negative-return footgun exists in MADB_DsnToString (Windows prompt path) and catalog-query builders - should be reviewed as part of the same fix
  • Attachments: rogue_server.py, odbc_client.c, direct_version_check.c, harness.cpp, run_poc.sh, poc_evidence.txt

Impact

A malicious or MITM MariaDB/MySQL server corrupts client-process memory (out-of-bounds write) in any application using MariaDB Connector/ODBC, the moment it opens a connection and allocates a statement. The demonstrated effect is a reliable crash (denial of service) via an AddressSanitizer-confirmed heap out-of-bounds write.

The write offset is chosen by the attacker through the version-string length, and the strcpy_s sink (MADB_SetNativeError) copies attacker-influenced content (server error text) to that attacker-chosen backward offset, so corruption of adjacent heap objects - and potentially impact beyond DoS - is plausible on non-hardened builds. Only the crash is demonstrated here.

Blast radius: every application (BI tools, ETL jobs, application servers, isql, etc.) that connects through MariaDB Connector/ODBC. Preconditions: (a) client connects to a server the attacker controls or can MITM (driver default is plaintext - no TLS), and (b) the application allocates a statement, which every ODBC workflow does. No credentials, privileges, or human interaction beyond initiating the connection are required.

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