From 4b0dd469bbba7bf5f25f1a4f694aeb15c3515be4 Mon Sep 17 00:00:00 2001 From: Saki Takamachi Date: Sun, 3 May 2026 19:56:30 +0200 Subject: [PATCH 07/10] GHSA-w476-322c-wpvm: [pdo_firebird] Fix SQL injection via NUL bytes in quoted strings Fixes GHSA-w476-322c-wpvm Fixes CVE-2025-14179 (cherry picked from commit 3f40b65323dd1b85e9bab6878237d3867e449d5c) --- ext/pdo_firebird/firebird_driver.c | 69 ++++++++++++------- .../tests/ghsa-w476-322c-wpvm.phpt | 44 ++++++++++++ 2 files changed, 88 insertions(+), 25 deletions(-) create mode 100644 ext/pdo_firebird/tests/ghsa-w476-322c-wpvm.phpt diff --git a/ext/pdo_firebird/firebird_driver.c b/ext/pdo_firebird/firebird_driver.c index a446622c90e..25882919f86 100644 --- a/ext/pdo_firebird/firebird_driver.c +++ b/ext/pdo_firebird/firebird_driver.c @@ -291,7 +291,7 @@ static FbTokenType getToken(const char** begin, const char* end) return ret; } -int preprocess(const zend_string* sql, char* sql_out, HashTable* named_params) +int preprocess(const zend_string* sql, char* sql_out, size_t* sql_out_len, HashTable* named_params) { bool passAsIs = 1, execBlock = 0; zend_long pindex = -1; @@ -322,7 +322,7 @@ int preprocess(const zend_string* sql, char* sql_out, HashTable* named_params) if (l > 252) { return 0; } - strncpy(ident, i, l); + memcpy(ident, i, l); ident[l] = '\0'; if (!strcasecmp(ident, "EXECUTE")) { @@ -347,7 +347,7 @@ int preprocess(const zend_string* sql, char* sql_out, HashTable* named_params) if (l > 252) { return 0; } - strncpy(ident2, i2, l); + memcpy(ident2, i2, l); ident2[l] = '\0'; execBlock = !strcasecmp(ident2, "BLOCK"); passAsIs = 0; @@ -363,11 +363,15 @@ int preprocess(const zend_string* sql, char* sql_out, HashTable* named_params) if (passAsIs) { - strcpy(sql_out, ZSTR_VAL(sql)); + memcpy(sql_out, ZSTR_VAL(sql), ZSTR_LEN(sql)); + sql_out[ZSTR_LEN(sql)] = '\0'; + *sql_out_len = ZSTR_LEN(sql); return 1; } - strncat(sql_out, start, p - start); + char *sql_out_p = sql_out; + memcpy(sql_out_p, start, p - start); + sql_out_p += p - start; while (p < end) { @@ -375,10 +379,12 @@ int preprocess(const zend_string* sql, char* sql_out, HashTable* named_params) tok = getToken(&p, end); switch (tok) { - case ttParamMark: - tok = getToken(&p, end); + case ttParamMark: { + const char* p_peek = p; + tok = getToken(&p_peek, end); if (tok == ttIdent /*|| tok == ttString*/) { + p = p_peek; ++pindex; l = p - start; /* check the length of the identifier */ @@ -387,7 +393,7 @@ int preprocess(const zend_string* sql, char* sql_out, HashTable* named_params) if (l > 253) { return 0; } - strncpy(pname, start, l); + memcpy(pname, start, l); pname[l] = '\0'; if (named_params) { @@ -396,7 +402,7 @@ int preprocess(const zend_string* sql, char* sql_out, HashTable* named_params) zend_hash_str_update(named_params, pname, l, &tmp); } - strcat(sql_out, "?"); + *sql_out_p++ = '?'; } else { @@ -406,10 +412,11 @@ int preprocess(const zend_string* sql, char* sql_out, HashTable* named_params) return 0; } ++pindex; - strncat(sql_out, start, p - start); + memcpy(sql_out_p, start, p - start); + sql_out_p += p - start; } break; - + } case ttIdent: if (execBlock) { @@ -421,11 +428,14 @@ int preprocess(const zend_string* sql, char* sql_out, HashTable* named_params) if (l > 252) { return 0; } - strncpy(ident, start, l); + memcpy(ident, start, l); ident[l] = '\0'; if (!strcasecmp(ident, "AS")) { - strncat(sql_out, start, end - start); + memcpy(sql_out_p, start, end - start); + sql_out_p += end - start; + *sql_out_p = '\0'; + *sql_out_len = sql_out_p - sql_out; return 1; } } @@ -436,7 +446,8 @@ int preprocess(const zend_string* sql, char* sql_out, HashTable* named_params) case ttComment: case ttString: case ttOther: - strncat(sql_out, start, p - start); + memcpy(sql_out_p, start, p - start); + sql_out_p += p - start; break; case ttBrokenComment: @@ -454,6 +465,8 @@ int preprocess(const zend_string* sql, char* sql_out, HashTable* named_params) break; } } + *sql_out_p = '\0'; + *sql_out_len = sql_out_p - sql_out; return 1; } @@ -663,7 +676,7 @@ free_statement: static zend_string* firebird_handle_quoter(pdo_dbh_t *dbh, const zend_string *unquoted, enum pdo_param_type paramtype) { size_t qcount = 0; - char const *co, *l, *r; + char const *co, *l; char *c; size_t quotedlen; zend_string *quoted_str; @@ -672,9 +685,15 @@ static zend_string* firebird_handle_quoter(pdo_dbh_t *dbh, const zend_string *un return zend_string_init("''", 2, 0); } + const char * const end = ZSTR_VAL(unquoted) + ZSTR_LEN(unquoted); + /* Firebird only requires single quotes to be doubled if string lengths are used */ /* count the number of ' characters */ - for (co = ZSTR_VAL(unquoted); (co = strchr(co,'\'')); qcount++, co++); + for (co = ZSTR_VAL(unquoted); co < end; co++) { + if (*co == '\'') { + qcount++; + } + } if (UNEXPECTED(ZSTR_LEN(unquoted) + 2 > ZSTR_MAX_LEN - qcount)) { return NULL; @@ -686,15 +705,14 @@ static zend_string* firebird_handle_quoter(pdo_dbh_t *dbh, const zend_string *un *c++ = '\''; /* foreach (chunk that ends in a quote) */ - for (l = ZSTR_VAL(unquoted); (r = strchr(l,'\'')); l = r+1) { - strncpy(c, l, r-l+1); - c += (r-l+1); - /* add the second quote */ - *c++ = '\''; + for (l = ZSTR_VAL(unquoted); l < end; l++) { + *c++ = *l; + if (*l == '\'') { + /* add the second quote */ + *c++ = '\''; + } } - /* copy the remainder */ - strncpy(c, l, quotedlen-(c-ZSTR_VAL(quoted_str))-1); ZSTR_VAL(quoted_str)[quotedlen-1] = '\''; ZSTR_VAL(quoted_str)[quotedlen] = '\0'; @@ -787,6 +805,7 @@ static int firebird_alloc_prepare_stmt(pdo_dbh_t *dbh, const zend_string *sql, { pdo_firebird_db_handle *H = (pdo_firebird_db_handle *)dbh->driver_data; char *new_sql; + size_t new_sql_len; /* Firebird allows SQL statements up to 64k, so bail if it doesn't fit */ if (ZSTR_LEN(sql) > 65536) { @@ -814,14 +833,14 @@ static int firebird_alloc_prepare_stmt(pdo_dbh_t *dbh, const zend_string *sql, we need to replace :foo by ?, and store the name we just replaced */ new_sql = emalloc(ZSTR_LEN(sql)+1); new_sql[0] = '\0'; - if (!preprocess(sql, new_sql, named_params)) { + if (!preprocess(sql, new_sql, &new_sql_len, named_params)) { strcpy(dbh->error_code, "07000"); efree(new_sql); return 0; } /* prepare the statement */ - if (isc_dsql_prepare(H->isc_status, &H->tr, s, 0, new_sql, H->sql_dialect, out_sqlda)) { + if (isc_dsql_prepare(H->isc_status, &H->tr, s, new_sql_len, new_sql, H->sql_dialect, out_sqlda)) { RECORD_ERROR(dbh); efree(new_sql); return 0; diff --git a/ext/pdo_firebird/tests/ghsa-w476-322c-wpvm.phpt b/ext/pdo_firebird/tests/ghsa-w476-322c-wpvm.phpt new file mode 100644 index 00000000000..41c1125e9b9 --- /dev/null +++ b/ext/pdo_firebird/tests/ghsa-w476-322c-wpvm.phpt @@ -0,0 +1,44 @@ +--TEST-- +GHSA-w476-322c-wpvm: SQL injection in pdo_firebird via NUL bytes in quoted strings +--EXTENSIONS-- +pdo_firebird +--SKIPIF-- + +--XLEAK-- +A bug in firebird causes a memory leak when calling `isc_attach_database()`. +See https://github.com/FirebirdSQL/firebird/issues/7849 +--FILE-- +exec('CREATE TABLE ghsa_w476_322c_wpvm (name VARCHAR(255))'); + +$param = $dbh->quote("\0"); +$param2 = $dbh->quote('or 1=1--'); +var_export($param); +echo("\n"); + +echo "prepare: "; +$stmt = $dbh->prepare("SELECT * FROM ghsa_w476_322c_wpvm WHERE name = {$param} AND name = {$param2}"); +$stmt->execute(); +echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC)) . "\n"; + +echo "query: "; +$stmt = $dbh->query("SELECT * FROM ghsa_w476_322c_wpvm WHERE name = {$param} AND name = {$param2}"); +echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC)) . "\n"; + +echo "exec: "; +$affectedRows = $dbh->exec("UPDATE ghsa_w476_322c_wpvm SET name = 'updated' WHERE name = {$param} AND name = {$param2}"); +echo $affectedRows . "\n"; +?> +--CLEAN-- +exec("DROP TABLE ghsa_w476_322c_wpvm"); +?> +--EXPECT-- +'\'' . "\0" . '\'' +prepare: [] +query: [] +exec: 0 -- 2.54.0