Skip to content

Commit 8e4daef

Browse files
committed
Fix phpGH-22668: odbc heap over-read on oversized column value
ext/odbc bound a displaysize+1 buffer per column but built the result string from the driver-reported vallen. A conforming driver truncates an over-long value into the buffer yet reports the full length, so the fetch over-read past the allocation and returned adjacent heap bytes to userland. Store the bound capacity in odbc_result_value and clamp the string length to it at the three bound-buffer fetch sites (odbc_fetch_array/object, odbc_fetch_into, odbc_result); the SQLGetData long-column paths stay bounded by longreadlen. Fixes phpGH-22668 Closes phpGH-22670
1 parent df77309 commit 8e4daef

5 files changed

Lines changed: 58 additions & 3 deletions

File tree

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ PHP NEWS
3838
. Fixed IntlChar methods leaving stale global error state after successful
3939
calls. (Xuyang Zhang)
4040

41+
- ODBC:
42+
. Fixed bug GH-22668 (Heap buffer over-read when a column value exceeds the
43+
driver-reported display size). (iliaal)
44+
4145
- OpenSSL:
4246
. Fixed timeout for supplemental read at end of a blocking stream in SSL
4347
stream wrapper. (ilutov)

ext/odbc/php_odbc.c

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,7 @@ void odbc_bindcols(odbc_result *result)
680680

681681
for(i = 0; i < result->numcols; i++) {
682682
charextraalloc = 0;
683+
result->values[i].value_max_len = 0;
683684
colfieldid = SQL_COLUMN_DISPLAY_SIZE;
684685

685686
rc = PHP_ODBC_SQLCOLATTRIBUTE(result->stmt, (SQLUSMALLINT)(i+1), PHP_ODBC_SQL_DESC_NAME,
@@ -706,6 +707,7 @@ void odbc_bindcols(odbc_result *result)
706707
#ifdef HAVE_ADABAS
707708
case SQL_TIMESTAMP:
708709
result->values[i].value = (char *)emalloc(27);
710+
result->values[i].value_max_len = 26;
709711
SQLBindCol(result->stmt, (SQLUSMALLINT)(i+1), SQL_C_CHAR, result->values[i].value,
710712
27, &result->values[i].vallen);
711713
break;
@@ -775,6 +777,7 @@ void odbc_bindcols(odbc_result *result)
775777
displaysize *= 4;
776778
}
777779
result->values[i].value = (char *)emalloc(displaysize + 1);
780+
result->values[i].value_max_len = displaysize;
778781
rc = SQLBindCol(result->stmt, (SQLUSMALLINT)(i+1), SQL_C_CHAR, result->values[i].value,
779782
displaysize + 1, &result->values[i].vallen);
780783
break;
@@ -1492,7 +1495,11 @@ static void php_odbc_fetch_hash(INTERNAL_FUNCTION_PARAMETERS, int result_type)
14921495
ZVAL_FALSE(&tmp);
14931496
break;
14941497
}
1495-
ZVAL_STRINGL(&tmp, result->values[i].value, result->values[i].vallen);
1498+
SQLLEN str_len = result->values[i].vallen;
1499+
if (str_len > result->values[i].value_max_len) {
1500+
str_len = result->values[i].value_max_len;
1501+
}
1502+
ZVAL_STRINGL(&tmp, result->values[i].value, str_len);
14961503
break;
14971504
}
14981505

@@ -1660,7 +1667,11 @@ PHP_FUNCTION(odbc_fetch_into)
16601667
ZVAL_FALSE(&tmp);
16611668
break;
16621669
}
1663-
ZVAL_STRINGL(&tmp, result->values[i].value, result->values[i].vallen);
1670+
SQLLEN str_len = result->values[i].vallen;
1671+
if (str_len > result->values[i].value_max_len) {
1672+
str_len = result->values[i].value_max_len;
1673+
}
1674+
ZVAL_STRINGL(&tmp, result->values[i].value, str_len);
16641675
break;
16651676
}
16661677
zend_hash_index_update(Z_ARRVAL_P(pv_res_arr), i, &tmp);
@@ -1910,7 +1921,11 @@ PHP_FUNCTION(odbc_result)
19101921
php_error_docref(NULL, E_WARNING, "Cannot get data of column #%d (driver cannot determine length)", field_ind + 1);
19111922
RETURN_FALSE;
19121923
} else {
1913-
RETURN_STRINGL(result->values[field_ind].value, result->values[field_ind].vallen);
1924+
SQLLEN str_len = result->values[field_ind].vallen;
1925+
if (str_len > result->values[field_ind].value_max_len) {
1926+
str_len = result->values[field_ind].value_max_len;
1927+
}
1928+
RETURN_STRINGL(result->values[field_ind].value, str_len);
19141929
}
19151930
break;
19161931
}

ext/odbc/php_odbc_includes.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ typedef struct odbc_result_value {
205205
char name[256];
206206
char *value;
207207
SQLLEN vallen;
208+
SQLLEN value_max_len;
208209
SQLLEN coltype;
209210
} odbc_result_value;
210211

ext/odbc/tests/config.inc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php
22

3+
defined('ODBC_SQLITE_DSN') || define('ODBC_SQLITE_DSN', "Driver=SQLite3;Database=:memory:");
4+
35
$dsn = getenv("ODBC_TEST_DSN");
46
$user = getenv("ODBC_TEST_USER");
57
$pass = getenv("ODBC_TEST_PASS");

ext/odbc/tests/gh22668.phpt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
--TEST--
2+
GH-22668 (Heap buffer over-read when a column value exceeds the bound buffer)
3+
--EXTENSIONS--
4+
odbc
5+
--SKIPIF--
6+
<?php
7+
include __DIR__ . "/config.inc";
8+
$conn = @odbc_connect(ODBC_SQLITE_DSN, null, null);
9+
if (!$conn) {
10+
die("skip requires the SQLite3 ODBC driver");
11+
}
12+
?>
13+
--FILE--
14+
<?php
15+
include __DIR__ . "/config.inc";
16+
$conn = odbc_connect(ODBC_SQLITE_DSN, null, null);
17+
18+
// The SQLite3 driver reports a 255 byte display size for a computed column, so
19+
// the bound buffer holds at most 255 bytes while the value is far longer. A
20+
// conforming driver truncates into the buffer but reports the full length; the
21+
// returned string must stay within the buffer, not over-read past it.
22+
$result = odbc_exec($conn, "SELECT printf('%.*c', 4096, 'A') AS data");
23+
$row = odbc_fetch_array($result);
24+
$s = $row['data'];
25+
26+
echo "clamped to buffer: "; var_dump(strlen($s) < 4096);
27+
echo "only value bytes: "; var_dump(strlen($s) === substr_count($s, 'A'));
28+
29+
odbc_close($conn);
30+
?>
31+
--EXPECT--
32+
clamped to buffer: bool(true)
33+
only value bytes: bool(true)

0 commit comments

Comments
 (0)