Skip to content

Commit 504c4a3

Browse files
SakiTakamachiericmann
authored andcommitted
GHSA-w476-322c-wpvm: [pdo_firebird] Fix SQL injection via NUL bytes in quoted strings
Fixes GHSA-w476-322c-wpvm Fixes CVE-2025-14179
1 parent ccb9ec6 commit 504c4a3

2 files changed

Lines changed: 88 additions & 25 deletions

File tree

ext/pdo_firebird/firebird_driver.c

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ static FbTokenType getToken(const char** begin, const char* end)
293293
return ret;
294294
}
295295

296-
int preprocess(const zend_string* sql, char* sql_out, HashTable* named_params)
296+
int preprocess(const zend_string* sql, char* sql_out, size_t* sql_out_len, HashTable* named_params)
297297
{
298298
bool passAsIs = 1, execBlock = 0;
299299
zend_long pindex = -1;
@@ -324,7 +324,7 @@ int preprocess(const zend_string* sql, char* sql_out, HashTable* named_params)
324324
if (l > 252) {
325325
return 0;
326326
}
327-
strncpy(ident, i, l);
327+
memcpy(ident, i, l);
328328
ident[l] = '\0';
329329
if (!strcasecmp(ident, "EXECUTE"))
330330
{
@@ -349,7 +349,7 @@ int preprocess(const zend_string* sql, char* sql_out, HashTable* named_params)
349349
if (l > 252) {
350350
return 0;
351351
}
352-
strncpy(ident2, i2, l);
352+
memcpy(ident2, i2, l);
353353
ident2[l] = '\0';
354354
execBlock = !strcasecmp(ident2, "BLOCK");
355355
passAsIs = 0;
@@ -365,22 +365,28 @@ int preprocess(const zend_string* sql, char* sql_out, HashTable* named_params)
365365

366366
if (passAsIs)
367367
{
368-
strcpy(sql_out, ZSTR_VAL(sql));
368+
memcpy(sql_out, ZSTR_VAL(sql), ZSTR_LEN(sql));
369+
sql_out[ZSTR_LEN(sql)] = '\0';
370+
*sql_out_len = ZSTR_LEN(sql);
369371
return 1;
370372
}
371373

372-
strncat(sql_out, start, p - start);
374+
char *sql_out_p = sql_out;
375+
memcpy(sql_out_p, start, p - start);
376+
sql_out_p += p - start;
373377

374378
while (p < end)
375379
{
376380
start = p;
377381
tok = getToken(&p, end);
378382
switch (tok)
379383
{
380-
case ttParamMark:
381-
tok = getToken(&p, end);
384+
case ttParamMark: {
385+
const char* p_peek = p;
386+
tok = getToken(&p_peek, end);
382387
if (tok == ttIdent /*|| tok == ttString*/)
383388
{
389+
p = p_peek;
384390
++pindex;
385391
l = p - start;
386392
/* check the length of the identifier */
@@ -389,7 +395,7 @@ int preprocess(const zend_string* sql, char* sql_out, HashTable* named_params)
389395
if (l > 253) {
390396
return 0;
391397
}
392-
strncpy(pname, start, l);
398+
memcpy(pname, start, l);
393399
pname[l] = '\0';
394400

395401
if (named_params) {
@@ -398,7 +404,7 @@ int preprocess(const zend_string* sql, char* sql_out, HashTable* named_params)
398404
zend_hash_str_update(named_params, pname, l, &tmp);
399405
}
400406

401-
strcat(sql_out, "?");
407+
*sql_out_p++ = '?';
402408
}
403409
else
404410
{
@@ -408,10 +414,11 @@ int preprocess(const zend_string* sql, char* sql_out, HashTable* named_params)
408414
return 0;
409415
}
410416
++pindex;
411-
strncat(sql_out, start, p - start);
417+
memcpy(sql_out_p, start, p - start);
418+
sql_out_p += p - start;
412419
}
413420
break;
414-
421+
}
415422
case ttIdent:
416423
if (execBlock)
417424
{
@@ -423,11 +430,14 @@ int preprocess(const zend_string* sql, char* sql_out, HashTable* named_params)
423430
if (l > 252) {
424431
return 0;
425432
}
426-
strncpy(ident, start, l);
433+
memcpy(ident, start, l);
427434
ident[l] = '\0';
428435
if (!strcasecmp(ident, "AS"))
429436
{
430-
strncat(sql_out, start, end - start);
437+
memcpy(sql_out_p, start, end - start);
438+
sql_out_p += end - start;
439+
*sql_out_p = '\0';
440+
*sql_out_len = sql_out_p - sql_out;
431441
return 1;
432442
}
433443
}
@@ -438,7 +448,8 @@ int preprocess(const zend_string* sql, char* sql_out, HashTable* named_params)
438448
case ttComment:
439449
case ttString:
440450
case ttOther:
441-
strncat(sql_out, start, p - start);
451+
memcpy(sql_out_p, start, p - start);
452+
sql_out_p += p - start;
442453
break;
443454

444455
case ttBrokenComment:
@@ -456,6 +467,8 @@ int preprocess(const zend_string* sql, char* sql_out, HashTable* named_params)
456467
break;
457468
}
458469
}
470+
*sql_out_p = '\0';
471+
*sql_out_len = sql_out_p - sql_out;
459472
return 1;
460473
}
461474

@@ -665,7 +678,7 @@ static zend_long firebird_handle_doer(pdo_dbh_t *dbh, const zend_string *sql) /*
665678
static zend_string* firebird_handle_quoter(pdo_dbh_t *dbh, const zend_string *unquoted, enum pdo_param_type paramtype)
666679
{
667680
size_t qcount = 0;
668-
char const *co, *l, *r;
681+
char const *co, *l;
669682
char *c;
670683
size_t quotedlen;
671684
zend_string *quoted_str;
@@ -674,9 +687,15 @@ static zend_string* firebird_handle_quoter(pdo_dbh_t *dbh, const zend_string *un
674687
return ZSTR_INIT_LITERAL("''", 0);
675688
}
676689

690+
const char * const end = ZSTR_VAL(unquoted) + ZSTR_LEN(unquoted);
691+
677692
/* Firebird only requires single quotes to be doubled if string lengths are used */
678693
/* count the number of ' characters */
679-
for (co = ZSTR_VAL(unquoted); (co = strchr(co,'\'')); qcount++, co++);
694+
for (co = ZSTR_VAL(unquoted); co < end; co++) {
695+
if (*co == '\'') {
696+
qcount++;
697+
}
698+
}
680699

681700
if (UNEXPECTED(ZSTR_LEN(unquoted) + 2 > ZSTR_MAX_LEN - qcount)) {
682701
return NULL;
@@ -688,15 +707,14 @@ static zend_string* firebird_handle_quoter(pdo_dbh_t *dbh, const zend_string *un
688707
*c++ = '\'';
689708

690709
/* foreach (chunk that ends in a quote) */
691-
for (l = ZSTR_VAL(unquoted); (r = strchr(l,'\'')); l = r+1) {
692-
strncpy(c, l, r-l+1);
693-
c += (r-l+1);
694-
/* add the second quote */
695-
*c++ = '\'';
710+
for (l = ZSTR_VAL(unquoted); l < end; l++) {
711+
*c++ = *l;
712+
if (*l == '\'') {
713+
/* add the second quote */
714+
*c++ = '\'';
715+
}
696716
}
697717

698-
/* copy the remainder */
699-
strncpy(c, l, quotedlen-(c-ZSTR_VAL(quoted_str))-1);
700718
ZSTR_VAL(quoted_str)[quotedlen-1] = '\'';
701719
ZSTR_VAL(quoted_str)[quotedlen] = '\0';
702720

@@ -789,6 +807,7 @@ static int firebird_alloc_prepare_stmt(pdo_dbh_t *dbh, const zend_string *sql,
789807
{
790808
pdo_firebird_db_handle *H = (pdo_firebird_db_handle *)dbh->driver_data;
791809
char *new_sql;
810+
size_t new_sql_len;
792811

793812
/* Firebird allows SQL statements up to 64k, so bail if it doesn't fit */
794813
if (ZSTR_LEN(sql) > 65536) {
@@ -816,14 +835,14 @@ static int firebird_alloc_prepare_stmt(pdo_dbh_t *dbh, const zend_string *sql,
816835
we need to replace :foo by ?, and store the name we just replaced */
817836
new_sql = emalloc(ZSTR_LEN(sql)+1);
818837
new_sql[0] = '\0';
819-
if (!preprocess(sql, new_sql, named_params)) {
838+
if (!preprocess(sql, new_sql, &new_sql_len, named_params)) {
820839
strcpy(dbh->error_code, "07000");
821840
efree(new_sql);
822841
return 0;
823842
}
824843

825844
/* prepare the statement */
826-
if (isc_dsql_prepare(H->isc_status, &H->tr, s, 0, new_sql, H->sql_dialect, out_sqlda)) {
845+
if (isc_dsql_prepare(H->isc_status, &H->tr, s, new_sql_len, new_sql, H->sql_dialect, out_sqlda)) {
827846
RECORD_ERROR(dbh);
828847
efree(new_sql);
829848
return 0;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
--TEST--
2+
GHSA-w476-322c-wpvm: SQL injection in pdo_firebird via NUL bytes in quoted strings
3+
--EXTENSIONS--
4+
pdo_firebird
5+
--SKIPIF--
6+
<?php require('skipif.inc'); ?>
7+
--XLEAK--
8+
A bug in firebird causes a memory leak when calling `isc_attach_database()`.
9+
See https://github.com/FirebirdSQL/firebird/issues/7849
10+
--FILE--
11+
<?php
12+
13+
require("testdb.inc");
14+
15+
$dbh->exec('CREATE TABLE ghsa_w476_322c_wpvm (name VARCHAR(255))');
16+
17+
$param = $dbh->quote("\0");
18+
$param2 = $dbh->quote('or 1=1--');
19+
var_export($param);
20+
echo("\n");
21+
22+
echo "prepare: ";
23+
$stmt = $dbh->prepare("SELECT * FROM ghsa_w476_322c_wpvm WHERE name = {$param} AND name = {$param2}");
24+
$stmt->execute();
25+
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC)) . "\n";
26+
27+
echo "query: ";
28+
$stmt = $dbh->query("SELECT * FROM ghsa_w476_322c_wpvm WHERE name = {$param} AND name = {$param2}");
29+
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC)) . "\n";
30+
31+
echo "exec: ";
32+
$affectedRows = $dbh->exec("UPDATE ghsa_w476_322c_wpvm SET name = 'updated' WHERE name = {$param} AND name = {$param2}");
33+
echo $affectedRows . "\n";
34+
?>
35+
--CLEAN--
36+
<?php
37+
require 'testdb.inc';
38+
$dbh->exec("DROP TABLE ghsa_w476_322c_wpvm");
39+
?>
40+
--EXPECT--
41+
'\'' . "\0" . '\''
42+
prepare: []
43+
query: []
44+
exec: 0

0 commit comments

Comments
 (0)