Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 75 additions & 14 deletions ext/pgsql/pgsql.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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, '.');
Expand Down
2 changes: 1 addition & 1 deletion ext/pgsql/tests/ghsa-hrwm-9436-5mv3.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 52 additions & 0 deletions ext/pgsql/tests/pg_copy_default_null_marker.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
--TEST--
pg_copy_to() / pg_copy_from() default null marker round-trip
--EXTENSIONS--
pgsql
--SKIPIF--
<?php include("inc/skipif.inc"); ?>
--FILE--
<?php

include('inc/config.inc');

$db = pg_connect($conn_str);
pg_query($db, 'DROP TABLE IF EXISTS pg_copy_default_null');
pg_query($db, 'CREATE TABLE pg_copy_default_null (id int, v text)');
pg_query($db, "INSERT INTO pg_copy_default_null VALUES (1, 'hello'), (2, NULL)");

$rows = pg_copy_to($db, 'pg_copy_default_null');
var_dump($rows);

pg_query($db, 'DELETE FROM pg_copy_default_null');
var_dump(pg_copy_from($db, 'pg_copy_default_null', $rows));
var_dump(pg_fetch_all(pg_query($db, 'SELECT v FROM pg_copy_default_null ORDER BY id')));

?>
--CLEAN--
<?php
include('inc/config.inc');
$db = pg_connect($conn_str);
pg_query($db, 'DROP TABLE IF EXISTS pg_copy_default_null');
?>
--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
}
}
43 changes: 43 additions & 0 deletions ext/pgsql/tests/pg_copy_from_null_as_escape.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
--TEST--
pg_copy_from() escapes the null_as argument
--EXTENSIONS--
pgsql
--SKIPIF--
<?php include("inc/skipif.inc"); ?>
--FILE--
<?php

include('inc/config.inc');

$db = pg_connect($conn_str);
pg_query($db, 'DROP TABLE IF EXISTS pg_copy_null_as_target');
pg_query($db, 'DROP TABLE IF EXISTS pg_copy_null_as_injected');
pg_query($db, 'CREATE TABLE pg_copy_null_as_target (v text)');

$evil = "X'; CREATE TABLE pg_copy_null_as_injected (v text); --";
var_dump(pg_copy_from($db, 'pg_copy_null_as_target', ["row\n"], "\t", $evil));

$r = pg_query($db, "SELECT 1 FROM pg_tables WHERE tablename = 'pg_copy_null_as_injected'");
var_dump(pg_num_rows($r));

$r = pg_query($db, 'SELECT v FROM pg_copy_null_as_target ORDER BY v');
var_dump(pg_fetch_all($r));

?>
--CLEAN--
<?php
include('inc/config.inc');
$db = pg_connect($conn_str);
pg_query($db, 'DROP TABLE IF EXISTS pg_copy_null_as_target');
pg_query($db, 'DROP TABLE IF EXISTS pg_copy_null_as_injected');
?>
--EXPECT--
bool(true)
int(0)
array(1) {
[0]=>
array(1) {
["v"]=>
string(3) "row"
}
}
36 changes: 36 additions & 0 deletions ext/pgsql/tests/pg_copy_from_table_name_escape.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
--TEST--
pg_copy_from() escapes the table name argument
--EXTENSIONS--
pgsql
--SKIPIF--
<?php include("inc/skipif.inc"); ?>
--FILE--
<?php

include('inc/config.inc');

$db = pg_connect($conn_str);
pg_query($db, 'DROP TABLE IF EXISTS pg_copy_from_target');
pg_query($db, 'CREATE TABLE pg_copy_from_target (v text)');
pg_query($db, 'DROP TABLE IF EXISTS pg_copy_from_other');
pg_query($db, 'CREATE TABLE pg_copy_from_other (v text)');

$evil = "pg_copy_from_other FROM STDIN --";
var_dump(pg_copy_from($db, $evil, ["redirected\n"]));

$rows = pg_fetch_all(pg_query($db, 'SELECT v FROM pg_copy_from_other')) ?: [];
var_dump($rows);

?>
--CLEAN--
<?php
include('inc/config.inc');
$db = pg_connect($conn_str);
pg_query($db, 'DROP TABLE IF EXISTS pg_copy_from_target');
pg_query($db, 'DROP TABLE IF EXISTS pg_copy_from_other');
?>
--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) {
}
60 changes: 60 additions & 0 deletions ext/pgsql/tests/pg_copy_to_query_injection.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
--TEST--
pg_copy_to() (query) source form: parens-wrap, already-parenthesised, and statement-injection rejection
--EXTENSIONS--
pgsql
--SKIPIF--
<?php include("inc/skipif.inc"); ?>
--FILE--
<?php

include('inc/config.inc');

$db = pg_connect($conn_str);
pg_query($db, 'DROP TABLE IF EXISTS pg_copy_to_qsource');
pg_query($db, 'DROP TABLE IF EXISTS pg_copy_to_qinjected');
pg_query($db, 'CREATE TABLE pg_copy_to_qsource (v text)');
pg_query($db, "INSERT INTO pg_copy_to_qsource VALUES ('a'), ('b')");

var_dump(pg_copy_to($db, '(SELECT v FROM pg_copy_to_qsource ORDER BY v)'));

var_dump(pg_copy_to($db, '((SELECT v FROM pg_copy_to_qsource ORDER BY v))'));

$evil = '(SELECT 1); DROP TABLE pg_copy_to_qsource; --';
var_dump(pg_copy_to($db, $evil));

$r = pg_query($db, "SELECT 1 FROM pg_tables WHERE tablename = 'pg_copy_to_qsource'");
var_dump(pg_num_rows($r));

$r = pg_query($db, "SELECT 1 FROM pg_tables WHERE tablename = 'pg_copy_to_qinjected'");
var_dump(pg_num_rows($r));

?>
--CLEAN--
<?php
include('inc/config.inc');
$db = pg_connect($conn_str);
pg_query($db, 'DROP TABLE IF EXISTS pg_copy_to_qsource');
pg_query($db, 'DROP TABLE IF EXISTS pg_copy_to_qinjected');
?>
--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)
34 changes: 34 additions & 0 deletions ext/pgsql/tests/pg_copy_to_table_name_escape.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
--TEST--
pg_copy_to() escapes the table name argument
--EXTENSIONS--
pgsql
--SKIPIF--
<?php include("inc/skipif.inc"); ?>
--FILE--
<?php

include('inc/config.inc');

$db = pg_connect($conn_str);
pg_query($db, 'DROP TABLE IF EXISTS pg_copy_to_source');
pg_query($db, 'DROP TABLE IF EXISTS pg_copy_to_injected');
pg_query($db, 'CREATE TABLE pg_copy_to_source (v text)');

$evil = "pg_copy_to_source; CREATE TABLE pg_copy_to_injected (v text); --";
var_dump(pg_copy_to($db, $evil));

$r = pg_query($db, "SELECT 1 FROM pg_tables WHERE tablename = 'pg_copy_to_injected'");
var_dump(pg_num_rows($r));

?>
--CLEAN--
<?php
include('inc/config.inc');
$db = pg_connect($conn_str);
pg_query($db, 'DROP TABLE IF EXISTS pg_copy_to_source');
pg_query($db, 'DROP TABLE IF EXISTS pg_copy_to_injected');
?>
--EXPECTF--
Warning: pg_copy_to(): Copy command failed: ERROR:%srelation "%s" does not exist%ain %s on line %d
bool(false)
int(0)
Loading