| 1 | Patch-Source: https://github.com/php/php-src/commit/ab048bd83b578119cf81b456526d50498421d617
|
| 2 | From c5ef5f06a0f823f6324a19e544abf647b5d89aaa Mon Sep 17 00:00:00 2001
|
| 3 | From: Ilija Tovilo <[email protected]>
|
| 4 | Date: Mon, 27 Jul 2026 16:49:38 +0200
|
| 5 | Subject: [PATCH 2/4] Fix SQL injection in ext-pgsql via E'...' backslash
|
| 6 | breakout
|
| 7 |
|
| 8 | php_pgsql_add_quotes() quotes the string with E'...', but PQescapeStringConn()
|
| 9 | does not escape \ unless standard_conforming_strings is off.
|
| 10 | PQescapeStringConn() is meant to be used with '', so do that instead.
|
| 11 |
|
| 12 | Fixes GHSA-7qpv-r5mr-78m4
|
| 13 |
|
| 14 | (cherry picked from commit ab048bd83b578119cf81b456526d50498421d617)
|
| 15 | ---
|
| 16 | ext/pgsql/pgsql.c | 3 +-
|
| 17 | ext/pgsql/tests/10pg_convert_9.phpt | 18 +++++-
|
| 18 | ext/pgsql/tests/10pg_convert_json_array.phpt | 4 +-
|
| 19 | ext/pgsql/tests/12pg_insert_9.phpt | 2 +-
|
| 20 | ext/pgsql/tests/14pg_update_9.phpt | 2 +-
|
| 21 | ext/pgsql/tests/GHSA-7qpv-r5mr-78m4.phpt | 58 ++++++++++++++++++++
|
| 22 | ext/pgsql/tests/bug64609.phpt | 2 +-
|
| 23 | ext/pgsql/tests/bug68638.phpt | 2 +-
|
| 24 | 8 files changed, 81 insertions(+), 10 deletions(-)
|
| 25 | create mode 100644 ext/pgsql/tests/GHSA-7qpv-r5mr-78m4.phpt
|
| 26 |
|
| 27 | diff --git a/ext/pgsql/pgsql.c b/ext/pgsql/pgsql.c
|
| 28 | index 01fb9dde3ba..de90b0b21d7 100644
|
| 29 | |
| 30 | |
| 31 | @@ -4563,7 +4563,7 @@ static int php_pgsql_convert_match(const zend_string *str, const char *regex , s
|
| 32 | */
|
| 33 | static zend_string *php_pgsql_add_quotes(zend_string *src)
|
| 34 | {
|
| 35 | - return zend_string_concat3("E'", strlen("E'"), ZSTR_VAL(src), ZSTR_LEN(src), "'", strlen("'"));
|
| 36 | + return zend_string_concat3("'", strlen("'"), ZSTR_VAL(src), ZSTR_LEN(src), "'", strlen("'"));
|
| 37 | }
|
| 38 | /* }}} */
|
| 39 |
|
| 40 | @@ -4841,7 +4841,6 @@ PHP_PGSQL_API zend_result php_pgsql_convert(PGconn *pg_link, const zend_string *
|
| 41 | zend_string *str;
|
| 42 | /* PostgreSQL ignores \0 */
|
| 43 | str = zend_string_alloc(Z_STRLEN_P(val) * 2, 0);
|
| 44 | - /* better to use PGSQLescapeLiteral since PGescapeStringConn does not handle special \ */
|
| 45 | ZSTR_LEN(str) = PQescapeStringConn(pg_link, ZSTR_VAL(str),
|
| 46 | Z_STRVAL_P(val), Z_STRLEN_P(val), &escape_err);
|
| 47 | if (escape_err) {
|
| 48 | diff --git a/ext/pgsql/tests/10pg_convert_9.phpt b/ext/pgsql/tests/10pg_convert_9.phpt
|
| 49 | index f4bfaf5e395..fb36e5801c8 100644
|
| 50 | |
| 51 | |
| 52 | @@ -21,6 +21,8 @@ $converted = pg_convert($db, $table_name, $fields);
|
| 53 |
|
| 54 | var_dump($converted);
|
| 55 |
|
| 56 | +var_dump(pg_convert($db, $table_name, ['str' => "\\' OR 1=1"]));
|
| 57 | +
|
| 58 | /* Invalid values */
|
| 59 | try {
|
| 60 | $converted = pg_convert($db, $table_name, [5 => 'AAA']);
|
| 61 | @@ -48,18 +50,30 @@ try {
|
| 62 | } catch (\TypeError $e) {
|
| 63 | echo $e->getMessage(), \PHP_EOL;
|
| 64 | }
|
| 65 | +
|
| 66 | +/* standard_conforming_strings = 1 */
|
| 67 | +pg_query($db, "SET standard_conforming_strings = 1");
|
| 68 | +var_dump(pg_convert($db, $table_name, ['str' => "\\' OR 1=1"]));
|
| 69 | ?>
|
| 70 | --EXPECT--
|
| 71 | array(3) {
|
| 72 | [""num""]=>
|
| 73 | string(4) "1234"
|
| 74 | [""str""]=>
|
| 75 | - string(6) "E'AAA'"
|
| 76 | + string(5) "'AAA'"
|
| 77 | [""bin""]=>
|
| 78 | - string(12) "E'\\x424242'"
|
| 79 | + string(11) "'\\x424242'"
|
| 80 | +}
|
| 81 | +array(1) {
|
| 82 | + [""str""]=>
|
| 83 | + string(13) "'\\'' OR 1=1'"
|
| 84 | }
|
| 85 | Array of values must be an associative array with string keys
|
| 86 | Array of values must be an associative array with string keys
|
| 87 | Values must be of type string|int|float|bool|null, array given
|
| 88 | Values must be of type string|int|float|bool|null, stdClass given
|
| 89 | Values must be of type string|int|float|bool|null, PgSql\Connection given
|
| 90 | +array(1) {
|
| 91 | + [""str""]=>
|
| 92 | + string(12) "'\'' OR 1=1'"
|
| 93 | +}
|
| 94 | diff --git a/ext/pgsql/tests/10pg_convert_json_array.phpt b/ext/pgsql/tests/10pg_convert_json_array.phpt
|
| 95 | index 918765db06c..5353e04dac5 100644
|
| 96 | |
| 97 | |
| 98 | @@ -32,8 +32,8 @@ if (!pg_insert($db, $table_name_92, $fields)) {
|
| 99 | --EXPECT--
|
| 100 | array(2) {
|
| 101 | [""textary""]=>
|
| 102 | - string(51) "E'{"meeting", "lunch", "training", "presentation"}'"
|
| 103 | + string(50) "'{"meeting", "lunch", "training", "presentation"}'"
|
| 104 | [""jsn""]=>
|
| 105 | - string(22) "E'{"f1":1,"f2":"foo"}'"
|
| 106 | + string(21) "'{"f1":1,"f2":"foo"}'"
|
| 107 | }
|
| 108 | OK
|
| 109 | diff --git a/ext/pgsql/tests/12pg_insert_9.phpt b/ext/pgsql/tests/12pg_insert_9.phpt
|
| 110 | index 892494fe6f0..699426c89b2 100644
|
| 111 | |
| 112 | |
| 113 | @@ -54,7 +54,7 @@ try {
|
| 114 | echo "Ok\n";
|
| 115 | ?>
|
| 116 | --EXPECTF--
|
| 117 | -INSERT INTO "php_pgsql_test" ("num","str","bin") VALUES (1234,E'AAA',E'\\x424242');
|
| 118 | +INSERT INTO "php_pgsql_test" ("num","str","bin") VALUES (1234,'AAA','\\x424242');
|
| 119 | INSERT INTO "php_pgsql_test" ("num","str","bin") VALUES ('1234','AAA','BBB');
|
| 120 | object(PgSql\Result)#%d (0) {
|
| 121 | }
|
| 122 | diff --git a/ext/pgsql/tests/14pg_update_9.phpt b/ext/pgsql/tests/14pg_update_9.phpt
|
| 123 | index d34f05216ec..64493075490 100644
|
| 124 | |
| 125 | |
| 126 | @@ -26,6 +26,6 @@ echo pg_update($db, $table_name, $fields, $ids, PGSQL_DML_STRING|PGSQL_DML_ESCAP
|
| 127 | echo "Ok\n";
|
| 128 | ?>
|
| 129 | --EXPECT--
|
| 130 | -UPDATE "php_pgsql_test" SET "num"=1234,"str"=E'ABC',"bin"=E'\\x58595a' WHERE "num"=1234;
|
| 131 | +UPDATE "php_pgsql_test" SET "num"=1234,"str"='ABC',"bin"='\\x58595a' WHERE "num"=1234;
|
| 132 | UPDATE "php_pgsql_test" SET "num"='1234',"str"='ABC',"bin"='XYZ' WHERE "num"='1234';
|
| 133 | Ok
|
| 134 | diff --git a/ext/pgsql/tests/GHSA-7qpv-r5mr-78m4.phpt b/ext/pgsql/tests/GHSA-7qpv-r5mr-78m4.phpt
|
| 135 | new file mode 100644
|
| 136 | index 00000000000..88b37a8432f
|
| 137 | |
| 138 | |
| 139 | @@ -0,0 +1,58 @@
|
| 140 | +--TEST--
|
| 141 | +GHSA-7qpv-r5mr-78m4: SQL injection via E'...' backslash breakout
|
| 142 | +--CREDITS--
|
| 143 | +expatch.llc
|
| 144 | +--EXTENSIONS--
|
| 145 | +pgsql
|
| 146 | +--SKIPIF--
|
| 147 | +<?php include("skipif.inc"); ?>
|
| 148 | +--FILE--
|
| 149 | +<?php
|
| 150 | +include 'config.inc';
|
| 151 | +
|
| 152 | +$db = pg_connect($conn_str);
|
| 153 | +
|
| 154 | +pg_query($db, "SET standard_conforming_strings = 1");
|
| 155 | +
|
| 156 | +pg_query($db, "DROP TABLE IF EXISTS ghsa_7qpv_r5mr_78m4");
|
| 157 | +pg_query($db, "CREATE TABLE ghsa_7qpv_r5mr_78m4 (id serial primary key, name text, admin boolean)");
|
| 158 | +pg_query($db, "INSERT INTO ghsa_7qpv_r5mr_78m4 (name, admin) VALUES ('alice', false), ('bob', false)");
|
| 159 | +
|
| 160 | +$params = ['name' => "zzz' OR 1=1 --"];
|
| 161 | +echo pg_select($db, 'ghsa_7qpv_r5mr_78m4', $params, PGSQL_DML_STRING) . "\n";
|
| 162 | +printf("returned: %d\n\n", count(pg_select($db, 'ghsa_7qpv_r5mr_78m4', $params)));
|
| 163 | +
|
| 164 | +$params = ['name' => "zzz\\' OR 1=1 --"];
|
| 165 | +echo pg_select($db, 'ghsa_7qpv_r5mr_78m4', $params, PGSQL_DML_STRING) . "\n";
|
| 166 | +printf("returned: %d\n\n", count(pg_select($db, 'ghsa_7qpv_r5mr_78m4', $params)));
|
| 167 | +
|
| 168 | +$params = ['name' => "john\\', true) --", 'admin' => 'false'];
|
| 169 | +echo pg_insert($db, 'ghsa_7qpv_r5mr_78m4', $params, PGSQL_DML_STRING) . "\n";
|
| 170 | +pg_insert($db, 'ghsa_7qpv_r5mr_78m4', $params);
|
| 171 | +var_dump(pg_select($db, 'ghsa_7qpv_r5mr_78m4', ['id' => 3])[0]['admin']);
|
| 172 | +echo "\n";
|
| 173 | +
|
| 174 | +$params = ['name' => "jake\\', true) --", 'admin' => 'f'];
|
| 175 | +echo pg_insert($db, 'ghsa_7qpv_r5mr_78m4', $params, PGSQL_DML_ESCAPE|PGSQL_DML_STRING) . "\n";
|
| 176 | +pg_insert($db, 'ghsa_7qpv_r5mr_78m4', $params, PGSQL_DML_EXEC|PGSQL_DML_ESCAPE);
|
| 177 | +var_dump(pg_select($db, 'ghsa_7qpv_r5mr_78m4', ['id' => 4])[0]['admin']);
|
| 178 | +
|
| 179 | +?>
|
| 180 | +--EXPECT--
|
| 181 | +SELECT * FROM "ghsa_7qpv_r5mr_78m4" WHERE "name"='zzz'' OR 1=1 --';
|
| 182 | +returned: 0
|
| 183 | +
|
| 184 | +SELECT * FROM "ghsa_7qpv_r5mr_78m4" WHERE "name"='zzz\'' OR 1=1 --';
|
| 185 | +returned: 0
|
| 186 | +
|
| 187 | +INSERT INTO "ghsa_7qpv_r5mr_78m4" ("name","admin") VALUES ('john\'', true) --','f');
|
| 188 | +string(1) "f"
|
| 189 | +
|
| 190 | +INSERT INTO "ghsa_7qpv_r5mr_78m4" ("name","admin") VALUES ('jake\'', true) --','f');
|
| 191 | +string(1) "f"
|
| 192 | +--CLEAN--
|
| 193 | +<?php
|
| 194 | +include('config.inc');
|
| 195 | +$db = pg_connect($conn_str);
|
| 196 | +pg_query($db, "DROP TABLE IF EXISTS ghsa_7qpv_r5mr_78m4");
|
| 197 | +?>
|
| 198 | diff --git a/ext/pgsql/tests/bug64609.phpt b/ext/pgsql/tests/bug64609.phpt
|
| 199 | index ebf878dbeed..ba27091ecf3 100644
|
| 200 | |
| 201 | |
| 202 | @@ -28,5 +28,5 @@ var_dump($converted);
|
| 203 | --EXPECT--
|
| 204 | array(1) {
|
| 205 | [""a""]=>
|
| 206 | - string(5) "E'ok'"
|
| 207 | + string(4) "'ok'"
|
| 208 | }
|
| 209 | diff --git a/ext/pgsql/tests/bug68638.phpt b/ext/pgsql/tests/bug68638.phpt
|
| 210 | index d95fa1e4412..2a1025cad8d 100644
|
| 211 | |
| 212 | |
| 213 | @@ -34,7 +34,7 @@ pg_query($conn, "DROP TABLE $table");
|
| 214 |
|
| 215 | ?>
|
| 216 | --EXPECT--
|
| 217 | -string(52) "UPDATE "test_68638" SET "value"=E'inf' WHERE "id"=1;"
|
| 218 | +string(51) "UPDATE "test_68638" SET "value"='inf' WHERE "id"=1;"
|
| 219 | array(2) {
|
| 220 | ["id"]=>
|
| 221 | string(1) "1"
|