Skip to content

Commit f535110

Browse files
committed
Reject substituting extension schemas or owners matching ["$'\].
Substituting such values in extension scripts facilitated SQL injection when @extowner@, @extschema@, or @extschema:...@ appeared inside a quoting construct (dollar quoting, '', or ""). No bundled extension was vulnerable. Vulnerable uses do appear in a documentation example and in non-bundled extensions. Hence, the attack prerequisite was an administrator having installed files of a vulnerable, trusted, non-bundled extension. Subject to that prerequisite, this enabled an attacker having database-level CREATE privilege to execute arbitrary code as the bootstrap superuser. By blocking this attack in the core server, there's no need to modify individual extensions. Back-patch to v11 (all supported versions). Reported by Micah Gate, Valerie Woolard, Tim Carey-Smith, and Christoph Berg. Security: CVE-2023-39417
1 parent e8386b2 commit f535110

File tree

7 files changed

+79
-15
lines changed

7 files changed

+79
-15
lines changed

src/backend/commands/extension.c

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,6 +1001,16 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control,
10011001
char *c_sql = read_extension_script_file(control, filename);
10021002
Datum t_sql;
10031003

1004+
/*
1005+
* We filter each substitution through quote_identifier(). When the
1006+
* arg contains one of the following characters, no one collection of
1007+
* quoting can work inside $$dollar-quoted string literals$$,
1008+
* 'single-quoted string literals', and outside of any literal. To
1009+
* avoid a security snare for extension authors, error on substitution
1010+
* for arguments containing these.
1011+
*/
1012+
const char *quoting_relevant_chars = "\"$'\\";
1013+
10041014
/* We use various functions that want to operate on text datums */
10051015
t_sql = CStringGetTextDatum(c_sql);
10061016

@@ -1030,6 +1040,11 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control,
10301040
t_sql,
10311041
CStringGetTextDatum("@extowner@"),
10321042
CStringGetTextDatum(qUserName));
1043+
if (strpbrk(userName, quoting_relevant_chars))
1044+
ereport(ERROR,
1045+
(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
1046+
errmsg("invalid character in extension owner: must not contain any of \"%s\"",
1047+
quoting_relevant_chars)));
10331048
}
10341049

10351050
/*
@@ -1041,13 +1056,19 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control,
10411056
*/
10421057
if (!control->relocatable)
10431058
{
1059+
Datum old = t_sql;
10441060
const char *qSchemaName = quote_identifier(schemaName);
10451061

10461062
t_sql = DirectFunctionCall3Coll(replace_text,
10471063
C_COLLATION_OID,
10481064
t_sql,
10491065
CStringGetTextDatum("@extschema@"),
10501066
CStringGetTextDatum(qSchemaName));
1067+
if (t_sql != old && strpbrk(schemaName, quoting_relevant_chars))
1068+
ereport(ERROR,
1069+
(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
1070+
errmsg("invalid character in extension \"%s\" schema: must not contain any of \"%s\"",
1071+
control->name, quoting_relevant_chars)));
10511072
}
10521073

10531074
/*
@@ -1057,6 +1078,7 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control,
10571078
Assert(list_length(control->requires) == list_length(requiredSchemas));
10581079
forboth(lc, control->requires, lc2, requiredSchemas)
10591080
{
1081+
Datum old = t_sql;
10601082
char *reqextname = (char *) lfirst(lc);
10611083
Oid reqschema = lfirst_oid(lc2);
10621084
char *schemaName = get_namespace_name(reqschema);
@@ -1069,6 +1091,11 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control,
10691091
t_sql,
10701092
CStringGetTextDatum(repltoken),
10711093
CStringGetTextDatum(qSchemaName));
1094+
if (t_sql != old && strpbrk(schemaName, quoting_relevant_chars))
1095+
ereport(ERROR,
1096+
(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
1097+
errmsg("invalid character in extension \"%s\" schema: must not contain any of \"%s\"",
1098+
reqextname, quoting_relevant_chars)));
10721099
}
10731100

10741101
/*

src/test/modules/test_extensions/Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ PGFILEDESC = "test_extensions - regression testing for EXTENSION support"
66
EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \
77
test_ext7 test_ext8 test_ext_cine test_ext_cor \
88
test_ext_cyclic1 test_ext_cyclic2 \
9+
test_ext_extschema \
910
test_ext_evttrig \
1011
test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3
1112

@@ -15,6 +16,7 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
1516
test_ext_cine--1.0.sql test_ext_cine--1.0--1.1.sql \
1617
test_ext_cor--1.0.sql \
1718
test_ext_cyclic1--1.0.sql test_ext_cyclic2--1.0.sql \
19+
test_ext_extschema--1.0.sql \
1820
test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql \
1921
test_ext_req_schema1--1.0.sql \
2022
test_ext_req_schema2--1.0.sql \

src/test/modules/test_extensions/expected/test_extensions.out

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
CREATE SCHEMA has$dollar;
12
-- test some errors
23
CREATE EXTENSION test_ext1;
34
ERROR: required extension "test_ext2" is not installed
@@ -6,35 +7,35 @@ CREATE EXTENSION test_ext1 SCHEMA test_ext1;
67
ERROR: schema "test_ext1" does not exist
78
CREATE EXTENSION test_ext1 SCHEMA test_ext;
89
ERROR: schema "test_ext" does not exist
9-
CREATE SCHEMA test_ext;
10-
CREATE EXTENSION test_ext1 SCHEMA test_ext;
10+
CREATE EXTENSION test_ext1 SCHEMA has$dollar;
1111
ERROR: extension "test_ext1" must be installed in schema "test_ext1"
1212
-- finally success
13-
CREATE EXTENSION test_ext1 SCHEMA test_ext CASCADE;
13+
CREATE EXTENSION test_ext1 SCHEMA has$dollar CASCADE;
1414
NOTICE: installing required extension "test_ext2"
1515
NOTICE: installing required extension "test_ext3"
1616
NOTICE: installing required extension "test_ext5"
1717
NOTICE: installing required extension "test_ext4"
1818
SELECT extname, nspname, extversion, extrelocatable FROM pg_extension e, pg_namespace n WHERE extname LIKE 'test_ext%' AND e.extnamespace = n.oid ORDER BY 1;
19-
extname | nspname | extversion | extrelocatable
20-
-----------+-----------+------------+----------------
21-
test_ext1 | test_ext1 | 1.0 | f
22-
test_ext2 | test_ext | 1.0 | t
23-
test_ext3 | test_ext | 1.0 | t
24-
test_ext4 | test_ext | 1.0 | t
25-
test_ext5 | test_ext | 1.0 | t
19+
extname | nspname | extversion | extrelocatable
20+
-----------+------------+------------+----------------
21+
test_ext1 | test_ext1 | 1.0 | f
22+
test_ext2 | has$dollar | 1.0 | t
23+
test_ext3 | has$dollar | 1.0 | t
24+
test_ext4 | has$dollar | 1.0 | t
25+
test_ext5 | has$dollar | 1.0 | t
2626
(5 rows)
2727

2828
CREATE EXTENSION test_ext_cyclic1 CASCADE;
2929
NOTICE: installing required extension "test_ext_cyclic2"
3030
ERROR: cyclic dependency detected between extensions "test_ext_cyclic1" and "test_ext_cyclic2"
31-
DROP SCHEMA test_ext CASCADE;
31+
DROP SCHEMA has$dollar CASCADE;
3232
NOTICE: drop cascades to 5 other objects
3333
DETAIL: drop cascades to extension test_ext3
3434
drop cascades to extension test_ext5
3535
drop cascades to extension test_ext2
3636
drop cascades to extension test_ext4
3737
drop cascades to extension test_ext1
38+
CREATE SCHEMA has$dollar;
3839
CREATE EXTENSION test_ext6;
3940
DROP EXTENSION test_ext6;
4041
CREATE EXTENSION test_ext6;
@@ -312,6 +313,13 @@ Objects in extension "test_ext_cine"
312313
table ext_cine_tab3
313314
(9 rows)
314315

316+
--
317+
-- Test @extschema@ syntax.
318+
--
319+
CREATE SCHEMA "has space";
320+
CREATE EXTENSION test_ext_extschema SCHEMA has$dollar;
321+
ERROR: invalid character in extension "test_ext_extschema" schema: must not contain any of ""$'\"
322+
CREATE EXTENSION test_ext_extschema SCHEMA "has space";
315323
--
316324
-- Test extension with objects outside the extension's schema.
317325
--
@@ -358,6 +366,11 @@ DROP SCHEMA test_func_dep3;
358366
--
359367
-- Test @extschema:extname@ syntax and no_relocate option
360368
--
369+
CREATE EXTENSION test_ext_req_schema1 SCHEMA has$dollar;
370+
CREATE EXTENSION test_ext_req_schema3 CASCADE;
371+
NOTICE: installing required extension "test_ext_req_schema2"
372+
ERROR: invalid character in extension "test_ext_req_schema1" schema: must not contain any of ""$'\"
373+
DROP EXTENSION test_ext_req_schema1;
361374
CREATE SCHEMA test_s_dep;
362375
CREATE EXTENSION test_ext_req_schema1 SCHEMA test_s_dep;
363376
CREATE EXTENSION test_ext_req_schema3 CASCADE;

src/test/modules/test_extensions/meson.build

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ test_install_data += files(
2727
'test_ext_cyclic1.control',
2828
'test_ext_cyclic2--1.0.sql',
2929
'test_ext_cyclic2.control',
30+
'test_ext_extschema--1.0.sql',
31+
'test_ext_extschema.control',
3032
'test_ext_evttrig--1.0--2.0.sql',
3133
'test_ext_evttrig--1.0.sql',
3234
'test_ext_evttrig.control',

src/test/modules/test_extensions/sql/test_extensions.sql

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
1+
CREATE SCHEMA has$dollar;
2+
13
-- test some errors
24
CREATE EXTENSION test_ext1;
35
CREATE EXTENSION test_ext1 SCHEMA test_ext1;
46
CREATE EXTENSION test_ext1 SCHEMA test_ext;
5-
CREATE SCHEMA test_ext;
6-
CREATE EXTENSION test_ext1 SCHEMA test_ext;
7+
CREATE EXTENSION test_ext1 SCHEMA has$dollar;
78

89
-- finally success
9-
CREATE EXTENSION test_ext1 SCHEMA test_ext CASCADE;
10+
CREATE EXTENSION test_ext1 SCHEMA has$dollar CASCADE;
1011

1112
SELECT extname, nspname, extversion, extrelocatable FROM pg_extension e, pg_namespace n WHERE extname LIKE 'test_ext%' AND e.extnamespace = n.oid ORDER BY 1;
1213

1314
CREATE EXTENSION test_ext_cyclic1 CASCADE;
1415

15-
DROP SCHEMA test_ext CASCADE;
16+
DROP SCHEMA has$dollar CASCADE;
17+
CREATE SCHEMA has$dollar;
1618

1719
CREATE EXTENSION test_ext6;
1820
DROP EXTENSION test_ext6;
@@ -210,6 +212,13 @@ ALTER EXTENSION test_ext_cine UPDATE TO '1.1';
210212

211213
\dx+ test_ext_cine
212214

215+
--
216+
-- Test @extschema@ syntax.
217+
--
218+
CREATE SCHEMA "has space";
219+
CREATE EXTENSION test_ext_extschema SCHEMA has$dollar;
220+
CREATE EXTENSION test_ext_extschema SCHEMA "has space";
221+
213222
--
214223
-- Test extension with objects outside the extension's schema.
215224
--
@@ -245,6 +254,9 @@ DROP SCHEMA test_func_dep3;
245254
--
246255
-- Test @extschema:extname@ syntax and no_relocate option
247256
--
257+
CREATE EXTENSION test_ext_req_schema1 SCHEMA has$dollar;
258+
CREATE EXTENSION test_ext_req_schema3 CASCADE;
259+
DROP EXTENSION test_ext_req_schema1;
248260
CREATE SCHEMA test_s_dep;
249261
CREATE EXTENSION test_ext_req_schema1 SCHEMA test_s_dep;
250262
CREATE EXTENSION test_ext_req_schema3 CASCADE;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/* src/test/modules/test_extensions/test_ext_extschema--1.0.sql */
2+
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
3+
\echo Use "CREATE EXTENSION test_ext_extschema" to load this file. \quit
4+
5+
SELECT 1 AS @extschema@;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
comment = 'test @extschema@'
2+
default_version = '1.0'
3+
relocatable = false

0 commit comments

Comments
 (0)