diff --git a/ext/pgsql/pgsql.c b/ext/pgsql/pgsql.c index 8cb022c79cd1..e3435dd11273 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); @@ -3348,8 +3350,7 @@ PHP_FUNCTION(pg_copy_to) zend_string *table_name; zend_string *pg_delimiter = NULL; char *pg_null_as = "\\\\N"; - size_t pg_null_as_len = 0; - char *query; + size_t pg_null_as_len = sizeof("\\\\N") - 1; PGconn *pgsql; PGresult *pgsql_result; ExecStatusType status; @@ -3373,14 +3374,36 @@ PHP_FUNCTION(pg_copy_to) zend_argument_value_error(3, "must be one character"); RETURN_THROWS(); } + smart_str querystr = {0}; + smart_str_appends(&querystr, "COPY "); + if (ZSTR_LEN(table_name) > 0 && ZSTR_VAL(table_name)[0] == '(') { + smart_str_appendc(&querystr, '('); + smart_str_append(&querystr, table_name); + smart_str_appendc(&querystr, ')'); + } else if (build_tablename(&querystr, pgsql, table_name) == FAILURE) { + smart_str_free(&querystr); + RETURN_FALSE; + } - spprintf(&query, 0, "COPY %s TO STDOUT DELIMITER E'%c' NULL AS E'%s'", ZSTR_VAL(table_name), *ZSTR_VAL(pg_delimiter), pg_null_as); + char *escaped_delimiter = PQescapeLiteral(pgsql, ZSTR_VAL(pg_delimiter), 1); + char *escaped_null_as = PQescapeLiteral(pgsql, pg_null_as, pg_null_as_len); + if (!escaped_delimiter || !escaped_null_as) { + php_error_docref(NULL, E_WARNING, "Failed to escape COPY parameters"); + if (escaped_delimiter) PQfreemem(escaped_delimiter); + if (escaped_null_as) PQfreemem(escaped_null_as); + smart_str_free(&querystr); + RETURN_FALSE; + } + smart_str_append_printf(&querystr, " TO STDOUT DELIMITER %s NULL AS %s", escaped_delimiter, escaped_null_as); + smart_str_0(&querystr); + PQfreemem(escaped_delimiter); + PQfreemem(escaped_null_as); while ((pgsql_result = PQgetResult(pgsql))) { PQclear(pgsql_result); } - pgsql_result = PQexec(pgsql, query); - efree(query); + pgsql_result = PQexec(pgsql, ZSTR_VAL(querystr.s)); + smart_str_free(&querystr); if (pgsql_result) { status = PQresultStatus(pgsql_result); @@ -3463,8 +3486,7 @@ PHP_FUNCTION(pg_copy_from) zend_string *table_name; zend_string *pg_delimiter = NULL; char *pg_null_as = "\\\\N"; - size_t pg_null_as_len; - char *query; + size_t pg_null_as_len = sizeof("\\\\N") - 1; PGconn *pgsql; PGresult *pgsql_result; ExecStatusType status; @@ -3488,14 +3510,33 @@ PHP_FUNCTION(pg_copy_from) zend_argument_value_error(4, "must be one character"); RETURN_THROWS(); } + smart_str querystr = {0}; + smart_str_appends(&querystr, "COPY "); + if (build_tablename(&querystr, pgsql, table_name) == FAILURE) { + smart_str_free(&querystr); + RETURN_FALSE; + } + + char *escaped_delimiter = PQescapeLiteral(pgsql, ZSTR_VAL(pg_delimiter), 1); + char *escaped_null_as = PQescapeLiteral(pgsql, pg_null_as, pg_null_as_len); + if (!escaped_delimiter || !escaped_null_as) { + php_error_docref(NULL, E_WARNING, "Failed to escape COPY parameters"); + if (escaped_delimiter) PQfreemem(escaped_delimiter); + if (escaped_null_as) PQfreemem(escaped_null_as); + smart_str_free(&querystr); + RETURN_FALSE; + } + smart_str_append_printf(&querystr, " FROM STDIN DELIMITER %s NULL AS %s", escaped_delimiter, escaped_null_as); + smart_str_0(&querystr); + PQfreemem(escaped_delimiter); + PQfreemem(escaped_null_as); - 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); 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_null_as_escape.phpt b/ext/pgsql/tests/pg_copy_from_null_as_escape.phpt new file mode 100644 index 000000000000..31186186fe90 --- /dev/null +++ b/ext/pgsql/tests/pg_copy_from_null_as_escape.phpt @@ -0,0 +1,43 @@ +--TEST-- +pg_copy_from() escapes the null_as argument +--EXTENSIONS-- +pgsql +--SKIPIF-- + +--FILE-- + +--CLEAN-- + +--EXPECT-- +bool(true) +int(0) +array(1) { + [0]=> + array(1) { + ["v"]=> + string(3) "row" + } +} 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) { +} diff --git a/ext/pgsql/tests/pg_copy_to_query_injection.phpt b/ext/pgsql/tests/pg_copy_to_query_injection.phpt new file mode 100644 index 000000000000..de6bd6184d68 --- /dev/null +++ b/ext/pgsql/tests/pg_copy_to_query_injection.phpt @@ -0,0 +1,50 @@ +--TEST-- +pg_copy_to() rejects statement injection through the (query) source form +--EXTENSIONS-- +pgsql +--SKIPIF-- + +--FILE-- + +--CLEAN-- + +--EXPECTF-- +array(2) { + [0]=> + string(2) "a +" + [1]=> + string(2) "b +" +} + +Warning: pg_copy_to(): %ain %s on line %d +bool(false) +int(1) +int(0) diff --git a/ext/pgsql/tests/pg_copy_to_table_name_escape.phpt b/ext/pgsql/tests/pg_copy_to_table_name_escape.phpt new file mode 100644 index 000000000000..ada91026be75 --- /dev/null +++ b/ext/pgsql/tests/pg_copy_to_table_name_escape.phpt @@ -0,0 +1,34 @@ +--TEST-- +pg_copy_to() escapes the table name argument +--EXTENSIONS-- +pgsql +--SKIPIF-- + +--FILE-- + +--CLEAN-- + +--EXPECTF-- +Warning: pg_copy_to(): %s in %s on line %d +bool(false) +int(0)