From 1f0a1fa0b8bfcc55ea51cc5fb5f14277b91ff810 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Thu, 7 May 2026 22:21:59 -0400 Subject: [PATCH] ext/pgsql: route pg_copy_from table_name through build_tablename The COPY query for pg_copy_from embedded the table_name argument with a raw "%s", so a caller-supplied name like `t FROM STDIN --` redirected the data into a different table than the API documents. Use the same build_tablename helper that pg_insert/update/select/delete have used since bug #62978. pg_copy_to is unchanged because COPY ... TO accepts a parenthesised query as a source spec, which the existing test 06_bug73498 relies on. --- ext/pgsql/pgsql.c | 17 ++++++--- .../tests/pg_copy_from_table_name_escape.phpt | 36 +++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 ext/pgsql/tests/pg_copy_from_table_name_escape.phpt diff --git a/ext/pgsql/pgsql.c b/ext/pgsql/pgsql.c index 8cb022c79cd1..011d1b332d50 100644 --- a/ext/pgsql/pgsql.c +++ b/ext/pgsql/pgsql.c @@ -273,6 +273,8 @@ static void pgsql_lob_free_obj(zend_object *obj) /* Compatibility definitions */ +static inline zend_result build_tablename(smart_str *querystr, PGconn *pg_link, const zend_string *table); + static zend_string *_php_pgsql_trim_message(const char *message) { size_t i = strlen(message); @@ -3464,7 +3466,6 @@ PHP_FUNCTION(pg_copy_from) zend_string *pg_delimiter = NULL; char *pg_null_as = "\\\\N"; size_t pg_null_as_len; - char *query; PGconn *pgsql; PGresult *pgsql_result; ExecStatusType status; @@ -3489,13 +3490,21 @@ PHP_FUNCTION(pg_copy_from) RETURN_THROWS(); } - spprintf(&query, 0, "COPY %s FROM STDIN DELIMITER E'%c' NULL AS E'%s'", ZSTR_VAL(table_name), *ZSTR_VAL(pg_delimiter), pg_null_as); + smart_str querystr = {0}; + smart_str_appends(&querystr, "COPY "); + if (build_tablename(&querystr, pgsql, table_name) == FAILURE) { + smart_str_free(&querystr); + RETURN_FALSE; + } + smart_str_append_printf(&querystr, " FROM STDIN DELIMITER E'%c' NULL AS E'%s'", *ZSTR_VAL(pg_delimiter), pg_null_as); + smart_str_0(&querystr); + while ((pgsql_result = PQgetResult(pgsql))) { PQclear(pgsql_result); } - pgsql_result = PQexec(pgsql, query); + pgsql_result = PQexec(pgsql, ZSTR_VAL(querystr.s)); - efree(query); + smart_str_free(&querystr); if (pgsql_result) { status = PQresultStatus(pgsql_result); diff --git a/ext/pgsql/tests/pg_copy_from_table_name_escape.phpt b/ext/pgsql/tests/pg_copy_from_table_name_escape.phpt new file mode 100644 index 000000000000..defb7c6d3c81 --- /dev/null +++ b/ext/pgsql/tests/pg_copy_from_table_name_escape.phpt @@ -0,0 +1,36 @@ +--TEST-- +pg_copy_from() escapes the table name argument +--EXTENSIONS-- +pgsql +--SKIPIF-- + +--FILE-- + +--CLEAN-- + +--EXPECTF-- +Warning: pg_copy_from(): %s in %s on line %d +bool(false) +array(0) { +}