From abf5a10618537332f63194f2cf72b019c592029c Mon Sep 17 00:00:00 2001 From: Saki Takamachi Date: Sun, 3 May 2026 19:56:30 +0200 Subject: [PATCH 08/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) (cherry picked from commit 4b0dd469bbba7bf5f25f1a4f694aeb15c3515be4) --- ext/pdo_firebird/firebird_driver.c | 68 ++++++++++++------- .../tests/ghsa-w476-322c-wpvm.phpt | 44 ++++++++++++ 2 files changed, 88 insertions(+), 24 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 fb69797850..06a33651d7 100644 --- a/ext/pdo_firebird/firebird_driver.c +++ b/ext/pdo_firebird/firebird_driver.c @@ -290,7 +290,7 @@ static FbTokenType getToken(const char** begin, const char* end) return ret; } -int preprocess(const char* sql, int sql_len, char* sql_out, HashTable* named_params) +int preprocess(const char* sql, int sql_len, char* sql_out, size_t* sql_out_len, HashTable* named_params) { zend_bool passAsIs = 1, execBlock = 0; zend_long pindex = -1; @@ -321,7 +321,7 @@ int preprocess(const char* sql, int sql_len, char* sql_out, HashTable* named_par if (l > 252) { return 0; } - strncpy(ident, i, l); + memcpy(ident, i, l); ident[l] = '\0'; if (!strcasecmp(ident, "EXECUTE")) { @@ -346,7 +346,7 @@ int preprocess(const char* sql, int sql_len, char* sql_out, HashTable* named_par if (l > 252) { return 0; } - strncpy(ident2, i2, l); + memcpy(ident2, i2, l); ident2[l] = '\0'; execBlock = !strcasecmp(ident2, "BLOCK"); passAsIs = 0; @@ -362,11 +362,15 @@ int preprocess(const char* sql, int sql_len, char* sql_out, HashTable* named_par if (passAsIs) { - strcpy(sql_out, sql); + memcpy(sql_out, sql, sql_len); + sql_out[sql_len] = '\0'; + *sql_out_len = sql_len; 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) { @@ -374,10 +378,12 @@ int preprocess(const char* sql, int sql_len, char* sql_out, HashTable* named_par 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 */ @@ -386,7 +392,7 @@ int preprocess(const char* sql, int sql_len, char* sql_out, HashTable* named_par if (l > 253) { return 0; } - strncpy(pname, start, l); + memcpy(pname, start, l); pname[l] = '\0'; if (named_params) { @@ -395,7 +401,7 @@ int preprocess(const char* sql, int sql_len, char* sql_out, HashTable* named_par zend_hash_str_update(named_params, pname, l, &tmp); } - strcat(sql_out, "?"); + *sql_out_p++ = '?'; } else { @@ -405,10 +411,11 @@ int preprocess(const char* sql, int sql_len, char* sql_out, HashTable* named_par 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) { @@ -420,11 +427,14 @@ int preprocess(const char* sql, int sql_len, char* sql_out, HashTable* named_par 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; } } @@ -433,7 +443,8 @@ int preprocess(const char* sql, int sql_len, char* sql_out, HashTable* named_par 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: @@ -451,6 +462,8 @@ int preprocess(const char* sql, int sql_len, char* sql_out, HashTable* named_par break; } } + *sql_out_p = '\0'; + *sql_out_len = sql_out_p - sql_out; return 1; } @@ -664,7 +677,7 @@ static int firebird_handle_quoter(pdo_dbh_t *dbh, const char *unquoted, size_t u char **quoted, size_t *quotedlen, enum pdo_param_type paramtype) { size_t qcount = 0; - char const *co, *l, *r; + char const *co, *l; char *c; if (!unquotedlen) { @@ -674,9 +687,15 @@ static int firebird_handle_quoter(pdo_dbh_t *dbh, const char *unquoted, size_t u return 1; } + const char * const end = unquoted + unquotedlen; + /* Firebird only requires single quotes to be doubled if string lengths are used */ /* count the number of ' characters */ - for (co = unquoted; (co = strchr(co,'\'')); qcount++, co++); + for (co = unquoted; co < end; co++) { + if (*co == '\'') { + qcount++; + } + } if (UNEXPECTED(unquotedlen + 2 > ZSTR_MAX_LEN - qcount)) { return 0; @@ -687,15 +706,15 @@ static int firebird_handle_quoter(pdo_dbh_t *dbh, const char *unquoted, size_t u *c++ = '\''; /* foreach (chunk that ends in a quote) */ - for (l = unquoted; (r = strchr(l,'\'')); l = r+1) { - strncpy(c, l, r-l+1); - c += (r-l+1); - /* add the second quote */ - *c++ = '\''; + for (l = unquoted; l < end; l++) { + *c++ = *l; + if (*l == '\'') { + /* add the second quote */ + *c++ = '\''; + } } /* copy the remainder */ - strncpy(c, l, *quotedlen-(c-*quoted)-1); (*quoted)[*quotedlen-1] = '\''; (*quoted)[*quotedlen] = '\0'; @@ -788,6 +807,7 @@ static int firebird_alloc_prepare_stmt(pdo_dbh_t *dbh, const char *sql, size_t s { 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 (sql_len > 65536) { @@ -815,14 +835,14 @@ static int firebird_alloc_prepare_stmt(pdo_dbh_t *dbh, const char *sql, size_t s we need to replace :foo by ?, and store the name we just replaced */ new_sql = emalloc(sql_len+1); new_sql[0] = '\0'; - if (!preprocess(sql, sql_len, new_sql, named_params)) { + if (!preprocess(sql, sql_len, 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 0000000000..41c1125e9b --- /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