From 3f40b65323dd1b85e9bab6878237d3867e449d5c Mon Sep 17 00:00:00 2001
From: Saki Takamachi <saki@sakiot.com>
Date: Sun, 3 May 2026 19:56:30 +0200
Subject: [PATCH] GHSA-w476-322c-wpvm: [pdo_firebird] Fix SQL injection via NUL
 bytes in quoted strings

Fixes GHSA-w476-322c-wpvm
Fixes CVE-2025-14179
---
 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

Index: php-8.0.30/ext/pdo_firebird/firebird_driver.c
===================================================================
--- php-8.0.30.orig/ext/pdo_firebird/firebird_driver.c
+++ php-8.0.30/ext/pdo_firebird/firebird_driver.c
@@ -290,7 +290,7 @@ static FbTokenType getToken(const char**
 	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_
 	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_
 		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_
 
 	if (passAsIs)
 	{
-		strcpy(sql_out, 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)
 	{
@@ -374,10 +378,12 @@ int preprocess(const char* sql, int sql_
 		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_
 				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_
 					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_
 					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_
 				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_
 		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_
 			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_db
 	char **quoted, size_t *quotedlen, enum pdo_param_type paramtype)
 {
 	int qcount = 0;
-	char const *co, *l, *r;
+	char const *co, *l;
 	char *c;
 
 	if (!unquotedlen) {
@@ -674,24 +687,29 @@ static int firebird_handle_quoter(pdo_db
 		return 1;
 	}
 
+        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 = unquoted; (co = strchr(co,'\'')); qcount++, co++);
+	for (co = ZSTR_VAL(unquoted); co < end; co++) {
+		if (*co == '\'') {
+			qcount++;
+		}
+	}
 
 	*quotedlen = unquotedlen + qcount + 2;
 	*quoted = c = emalloc(*quotedlen+1);
 	*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 = ZSTR_VAL(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';
 
@@ -784,6 +802,7 @@ static int firebird_alloc_prepare_stmt(p
 {
 	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) {
@@ -811,14 +830,14 @@ static int firebird_alloc_prepare_stmt(p
 	   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, 0, new_sql_len, new_sql, H->sql_dialect, out_sqlda)) {
 		RECORD_ERROR(dbh);
 		efree(new_sql);
 		return 0;
