diff --git a/ext/pgsql/pgsql.c b/ext/pgsql/pgsql.c index 8cb022c79cd1..6732707c0c7d 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); @@ -3347,9 +3349,8 @@ PHP_FUNCTION(pg_copy_to) pgsql_link_handle *link; zend_string *table_name; zend_string *pg_delimiter = NULL; - char *pg_null_as = "\\\\N"; - size_t pg_null_as_len = 0; - char *query; + char *pg_null_as = "\\N"; + size_t pg_null_as_len = sizeof("\\N") - 1; PGconn *pgsql; PGresult *pgsql_result; ExecStatusType status; @@ -3373,14 +3374,44 @@ 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); + if (!escaped_delimiter) { + zend_string *msgbuf = _php_pgsql_trim_message(PQerrorMessage(pgsql)); + php_error_docref(NULL, E_WARNING, "Failed to escape delimiter '%c': %s", *ZSTR_VAL(pg_delimiter), ZSTR_VAL(msgbuf)); + zend_string_release(msgbuf); + smart_str_free(&querystr); + RETURN_FALSE; + } + char *escaped_null_as = PQescapeLiteral(pgsql, pg_null_as, pg_null_as_len); + if (!escaped_null_as) { + zend_string *msgbuf = _php_pgsql_trim_message(PQerrorMessage(pgsql)); + php_error_docref(NULL, E_WARNING, "Failed to escape null_as '%s': %s", pg_null_as, ZSTR_VAL(msgbuf)); + zend_string_release(msgbuf); + PQfreemem(escaped_delimiter); + 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); @@ -3462,9 +3493,8 @@ PHP_FUNCTION(pg_copy_from) zval *value; zend_string *table_name; zend_string *pg_delimiter = NULL; - char *pg_null_as = "\\\\N"; - size_t pg_null_as_len; - char *query; + char *pg_null_as = "\\N"; + size_t pg_null_as_len = sizeof("\\N") - 1; PGconn *pgsql; PGresult *pgsql_result; ExecStatusType status; @@ -3488,14 +3518,41 @@ 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); + if (!escaped_delimiter) { + zend_string *msgbuf = _php_pgsql_trim_message(PQerrorMessage(pgsql)); + php_error_docref(NULL, E_WARNING, "Failed to escape delimiter '%c': %s", *ZSTR_VAL(pg_delimiter), ZSTR_VAL(msgbuf)); + zend_string_release(msgbuf); + smart_str_free(&querystr); + RETURN_FALSE; + } + char *escaped_null_as = PQescapeLiteral(pgsql, pg_null_as, pg_null_as_len); + if (!escaped_null_as) { + zend_string *msgbuf = _php_pgsql_trim_message(PQerrorMessage(pgsql)); + php_error_docref(NULL, E_WARNING, "Failed to escape null_as '%s': %s", pg_null_as, ZSTR_VAL(msgbuf)); + zend_string_release(msgbuf); + PQfreemem(escaped_delimiter); + 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); @@ -5574,7 +5631,9 @@ static inline zend_result build_tablename(smart_str *querystr, PGconn *pg_link, } else { char *escaped = PQescapeIdentifier(pg_link, ZSTR_VAL(table), len); if (escaped == NULL) { - php_error_docref(NULL, E_NOTICE, "Failed to escape table name '%s'", ZSTR_VAL(table)); + zend_string *msgbuf = _php_pgsql_trim_message(PQerrorMessage(pg_link)); + php_error_docref(NULL, E_WARNING, "Failed to escape table name '%s': %s", ZSTR_VAL(table), ZSTR_VAL(msgbuf)); + zend_string_release(msgbuf); return FAILURE; } smart_str_appends(querystr, escaped); @@ -5590,7 +5649,9 @@ static inline zend_result build_tablename(smart_str *querystr, PGconn *pg_link, } else { char *escaped = PQescapeIdentifier(pg_link, after_dot, len); if (escaped == NULL) { - php_error_docref(NULL, E_NOTICE, "Failed to escape table name '%s'", ZSTR_VAL(table)); + zend_string *msgbuf = _php_pgsql_trim_message(PQerrorMessage(pg_link)); + php_error_docref(NULL, E_WARNING, "Failed to escape table name '%s': %s", ZSTR_VAL(table), ZSTR_VAL(msgbuf)); + zend_string_release(msgbuf); return FAILURE; } smart_str_appendc(querystr, '.'); diff --git a/ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt b/ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt index 6cbfe6d1f585..eedcb9951bfb 100644 --- a/ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt +++ b/ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt @@ -42,7 +42,7 @@ bool(false) Notice: pg_insert(): String value escaping failed for PostgreSQL 'text' (bar) in %s on line %d bool(false) -Notice: pg_insert(): Failed to escape table name 'ABC%s';' in %s on line %d +Warning: pg_insert(): Failed to escape table name 'ABC%s';': %s in %s on line %d bool(false) Notice: pg_insert(): Failed to escape field 'ABC%s';' in %s on line %d diff --git a/ext/pgsql/tests/pg_copy_default_null_marker.phpt b/ext/pgsql/tests/pg_copy_default_null_marker.phpt new file mode 100644 index 000000000000..81f5e248c21e --- /dev/null +++ b/ext/pgsql/tests/pg_copy_default_null_marker.phpt @@ -0,0 +1,52 @@ +--TEST-- +pg_copy_to() / pg_copy_from() default null marker round-trip +--EXTENSIONS-- +pgsql +--SKIPIF-- + +--FILE-- + +--CLEAN-- + +--EXPECT-- +array(2) { + [0]=> + string(8) "1 hello +" + [1]=> + string(5) "2 \N +" +} +bool(true) +array(2) { + [0]=> + array(1) { + ["v"]=> + string(5) "hello" + } + [1]=> + array(1) { + ["v"]=> + NULL + } +} 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..d98a93a90f95 --- /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(): Copy command failed: ERROR:%srelation "pg_copy_from_other FROM STDIN --" does not exist%ain %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..44a3fd558626 --- /dev/null +++ b/ext/pgsql/tests/pg_copy_to_query_injection.phpt @@ -0,0 +1,60 @@ +--TEST-- +pg_copy_to() (query) source form: parens-wrap, already-parenthesised, and statement-injection rejection +--EXTENSIONS-- +pgsql +--SKIPIF-- + +--FILE-- + +--CLEAN-- + +--EXPECTF-- +array(2) { + [0]=> + string(2) "a +" + [1]=> + string(2) "b +" +} +array(2) { + [0]=> + string(2) "a +" + [1]=> + string(2) "b +" +} + +Warning: pg_copy_to(): Copy command failed: ERROR:%ssyntax error at or near ";"%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..73e6a6d80217 --- /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(): Copy command failed: ERROR:%srelation "%s" does not exist%ain %s on line %d +bool(false) +int(0)