Skip to content

Commit 3a1e6e0

Browse files
committed
Fix GH-22666: pdo_odbc heap overflow on oversized output param
odbc_stmt_param_hook() sizes an output-param buffer from the declared maxlen but then copies the runtime value into it using the value's own length, and at fetch-back hands ZVAL_STRINGL the driver-reported length. Both trust a length that can exceed the buffer: the input copy overflows the heap (controlled write), and on a truncated OUTPUT value the driver reports the full length via the shared indicator, so the readback over-reads. Record the bound capacity in a new outbuflen field and clamp the copy and the readback to it. LOB output is rejected at bind, so those copy sites stay unreachable. Fixes GH-22666
1 parent ebed41a commit 3a1e6e0

4 files changed

Lines changed: 44 additions & 0 deletions

File tree

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ PHP NEWS
4343
- PDO_ODBC:
4444
. Fixed bug GH-20726 (Crash with ODBC connection pooling when the DSN
4545
carries no credentials). (iliaal)
46+
. Fixed bug GH-22666 (Heap buffer overflow when an output parameter value is
47+
longer than the declared maxlen). (iliaal)
4648

4749
- Phar:
4850
. Fixed inconsistent handling of the magic ".phar" directory. Paths such as

ext/pdo_odbc/odbc_stmt.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ static int odbc_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_data *p
360360
param->driver_data = P;
361361

362362
P->len = 0; /* is re-populated each EXEC_PRE */
363+
P->outbuflen = 0;
363364
P->outbuf = NULL;
364365

365366
P->is_unicode = pdo_odbc_sqltype_is_unicode(S, sqltype);
@@ -384,6 +385,7 @@ static int odbc_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_data *p
384385
P->len *= 2;
385386
}
386387
P->outbuf = emalloc(P->len + (P->is_unicode ? 2:1));
388+
P->outbuflen = P->len;
387389
}
388390
}
389391

@@ -481,10 +483,16 @@ static int odbc_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_data *p
481483
case PDO_ODBC_CONV_FAIL:
482484
case PDO_ODBC_CONV_NOT_REQUIRED:
483485
P->len = Z_STRLEN_P(parameter);
486+
if (P->len > P->outbuflen) {
487+
P->len = P->outbuflen;
488+
}
484489
memcpy(P->outbuf, Z_STRVAL_P(parameter), P->len);
485490
break;
486491
case PDO_ODBC_CONV_OK:
487492
P->len = ulen;
493+
if (P->len > P->outbuflen) {
494+
P->len = P->outbuflen;
495+
}
488496
memcpy(P->outbuf, S->convbuf, P->len);
489497
break;
490498
}
@@ -506,6 +514,9 @@ static int odbc_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_data *p
506514
zval_ptr_dtor(parameter);
507515

508516
if (P->len >= 0) {
517+
if (P->len > P->outbuflen) {
518+
P->len = P->outbuflen;
519+
}
509520
ZVAL_STRINGL(parameter, P->outbuf, P->len);
510521
switch (pdo_odbc_ucs22utf8(stmt, P->is_unicode, parameter)) {
511522
case PDO_ODBC_CONV_FAIL:

ext/pdo_odbc/php_pdo_odbc_int.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ typedef struct {
153153

154154
typedef struct {
155155
SQLLEN len;
156+
SQLLEN outbuflen;
156157
SQLSMALLINT paramtype;
157158
char *outbuf;
158159
unsigned is_unicode:1;

ext/pdo_odbc/tests/gh22666.phpt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
--TEST--
2+
GH-22666 (Heap buffer overflow when an output param value exceeds its maxlen)
3+
--EXTENSIONS--
4+
pdo_odbc
5+
--SKIPIF--
6+
<?php
7+
require __DIR__ . '/config.inc';
8+
try {
9+
$pdo = new PDO(PDO_ODBC_SQLITE_DSN);
10+
} catch (PDOException $e) {
11+
die("skip requires the SQLite3 ODBC driver");
12+
}
13+
?>
14+
--FILE--
15+
<?php
16+
require __DIR__ . '/config.inc';
17+
$pdo = new PDO(PDO_ODBC_SQLITE_DSN);
18+
19+
// An INPUT_OUTPUT parameter declares a maxlen of 4, so the output buffer is 4
20+
// bytes, but the runtime value is longer. The copy into that buffer must be
21+
// bounded to the declared capacity, not overflow it.
22+
$value = str_repeat('A', 64);
23+
$stmt = $pdo->prepare('SELECT ?');
24+
$stmt->bindParam(1, $value, PDO::PARAM_STR | PDO::PARAM_INPUT_OUTPUT, 4);
25+
$stmt->execute();
26+
27+
echo "bounded to maxlen: "; var_dump($value);
28+
?>
29+
--EXPECT--
30+
bounded to maxlen: string(4) "AAAA"

0 commit comments

Comments
 (0)