Reject substituting extension schemas or owners matching ["$'\].
authorNoah Misch <[email protected]>
Mon, 7 Aug 2023 13:05:56 +0000 (06:05 -0700)
committerNoah Misch <[email protected]>
Mon, 7 Aug 2023 13:05:56 +0000 (06:05 -0700)
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- to
v11 (all supported versions).

Reported by Micah Gate, Valerie Woolard, Tim Carey-Smith, and Christoph
Berg.

Security: CVE-2023-39417

src/backend/commands/extension.c
src/test/modules/test_extensions/Makefile
src/test/modules/test_extensions/expected/test_extensions.out
src/test/modules/test_extensions/meson.build
src/test/modules/test_extensions/sql/test_extensions.sql
src/test/modules/test_extensions/test_ext_extschema--1.0.sql[new file with mode: 0644]
src/test/modules/test_extensions/test_ext_extschema.control[new file with mode: 0644]

index 4cc994ca31e0b77970fe784d0c6afc523e8750db..535072d1819b642d6e71fced6c5fe3583a85766d 100644 (file)
@@ -996,6 +996,16 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control,
        char       *c_sql = read_extension_script_file(control, filename);
        Datum       t_sql;
 
+       /*
+        * We filter each substitution through quote_identifier().  When the
+        * arg contains one of the following characters, no one collection of
+        * quoting can work inside $$dollar-quoted string literals$$,
+        * 'single-quoted string literals', and outside of any literal.  To
+        * avoid a security snare for extension authors, error on substitution
+        * for arguments containing these.
+        */
+       const char *quoting_relevant_chars = "\"$'\\";
+
        /* We use various functions that want to operate on text datums */
        t_sql = CStringGetTextDatum(c_sql);
 
@@ -1025,6 +1035,11 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control,
                                            t_sql,
                                            CStringGetTextDatum("@extowner@"),
                                            CStringGetTextDatum(qUserName));
+           if (strpbrk(userName, quoting_relevant_chars))
+               ereport(ERROR,
+                       (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+                        errmsg("invalid character in extension owner: must not contain any of \"%s\"",
+                               quoting_relevant_chars)));
        }
 
        /*
@@ -1036,6 +1051,7 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control,
         */
        if (!control->relocatable)
        {
+           Datum       old = t_sql;
            const char *qSchemaName = quote_identifier(schemaName);
 
            t_sql = DirectFunctionCall3Coll(replace_text,
@@ -1043,6 +1059,11 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control,
                                            t_sql,
                                            CStringGetTextDatum("@extschema@"),
                                            CStringGetTextDatum(qSchemaName));
+           if (t_sql != old && strpbrk(schemaName, quoting_relevant_chars))
+               ereport(ERROR,
+                       (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+                        errmsg("invalid character in extension \"%s\" schema: must not contain any of \"%s\"",
+                               control->name, quoting_relevant_chars)));
        }
 
        /*
@@ -1052,6 +1073,7 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control,
        Assert(list_length(control->requires) == list_length(requiredSchemas));
        forboth(lc, control->requires, lc2, requiredSchemas)
        {
+           Datum       old = t_sql;
            char       *reqextname = (char *) lfirst(lc);
            Oid         reqschema = lfirst_oid(lc2);
            char       *schemaName = get_namespace_name(reqschema);
@@ -1064,6 +1086,11 @@ execute_extension_script(Oid extensionOid, ExtensionControlFile *control,
                                            t_sql,
                                            CStringGetTextDatum(repltoken),
                                            CStringGetTextDatum(qSchemaName));
+           if (t_sql != old && strpbrk(schemaName, quoting_relevant_chars))
+               ereport(ERROR,
+                       (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+                        errmsg("invalid character in extension \"%s\" schema: must not contain any of \"%s\"",
+                               reqextname, quoting_relevant_chars)));
        }
 
        /*
index 70fc0c8e66ced61b9426ac60f9b8bcab7dc2de45..1388c0fb0bdca56f0d27d6c95ea8d9903132f39d 100644 (file)
@@ -6,6 +6,7 @@ PGFILEDESC = "test_extensions - regression testing for EXTENSION support"
 EXTENSION = test_ext1 test_ext2 test_ext3 test_ext4 test_ext5 test_ext6 \
             test_ext7 test_ext8 test_ext_cine test_ext_cor \
             test_ext_cyclic1 test_ext_cyclic2 \
+            test_ext_extschema \
             test_ext_evttrig \
             test_ext_req_schema1 test_ext_req_schema2 test_ext_req_schema3
 
@@ -15,6 +16,7 @@ DATA = test_ext1--1.0.sql test_ext2--1.0.sql test_ext3--1.0.sql \
        test_ext_cine--1.0.sql test_ext_cine--1.0--1.1.sql \
        test_ext_cor--1.0.sql \
        test_ext_cyclic1--1.0.sql test_ext_cyclic2--1.0.sql \
+       test_ext_extschema--1.0.sql \
        test_ext_evttrig--1.0.sql test_ext_evttrig--1.0--2.0.sql \
        test_ext_req_schema1--1.0.sql \
        test_ext_req_schema2--1.0.sql \
index 254bd938be0c8ec7383648ea6c934827ce1e967b..472627a232b523d70ee2c794f9e6af10a7e8e263 100644 (file)
@@ -1,3 +1,4 @@
+CREATE SCHEMA has$dollar;
 -- test some errors
 CREATE EXTENSION test_ext1;
 ERROR:  required extension "test_ext2" is not installed
@@ -6,35 +7,35 @@ CREATE EXTENSION test_ext1 SCHEMA test_ext1;
 ERROR:  schema "test_ext1" does not exist
 CREATE EXTENSION test_ext1 SCHEMA test_ext;
 ERROR:  schema "test_ext" does not exist
-CREATE SCHEMA test_ext;
-CREATE EXTENSION test_ext1 SCHEMA test_ext;
+CREATE EXTENSION test_ext1 SCHEMA has$dollar;
 ERROR:  extension "test_ext1" must be installed in schema "test_ext1"
 -- finally success
-CREATE EXTENSION test_ext1 SCHEMA test_ext CASCADE;
+CREATE EXTENSION test_ext1 SCHEMA has$dollar CASCADE;
 NOTICE:  installing required extension "test_ext2"
 NOTICE:  installing required extension "test_ext3"
 NOTICE:  installing required extension "test_ext5"
 NOTICE:  installing required extension "test_ext4"
 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;
-  extname  |  nspname  | extversion | extrelocatable 
------------+-----------+------------+----------------
- test_ext1 | test_ext1 | 1.0        | f
- test_ext2 | test_ext  | 1.0        | t
- test_ext3 | test_ext  | 1.0        | t
- test_ext4 | test_ext  | 1.0        | t
- test_ext5 | test_ext  | 1.0        | t
+  extname  |  nspname   | extversion | extrelocatable 
+-----------+------------+------------+----------------
+ test_ext1 | test_ext1  | 1.0        | f
+ test_ext2 | has$dollar | 1.0        | t
+ test_ext3 | has$dollar | 1.0        | t
+ test_ext4 | has$dollar | 1.0        | t
+ test_ext5 | has$dollar | 1.0        | t
 (5 rows)
 
 CREATE EXTENSION test_ext_cyclic1 CASCADE;
 NOTICE:  installing required extension "test_ext_cyclic2"
 ERROR:  cyclic dependency detected between extensions "test_ext_cyclic1" and "test_ext_cyclic2"
-DROP SCHEMA test_ext CASCADE;
+DROP SCHEMA has$dollar CASCADE;
 NOTICE:  drop cascades to 5 other objects
 DETAIL:  drop cascades to extension test_ext3
 drop cascades to extension test_ext5
 drop cascades to extension test_ext2
 drop cascades to extension test_ext4
 drop cascades to extension test_ext1
+CREATE SCHEMA has$dollar;
 CREATE EXTENSION test_ext6;
 DROP EXTENSION test_ext6;
 CREATE EXTENSION test_ext6;
@@ -312,6 +313,13 @@ Objects in extension "test_ext_cine"
  table ext_cine_tab3
 (9 rows)
 
+--
+-- Test @extschema@ syntax.
+--
+CREATE SCHEMA "has space";
+CREATE EXTENSION test_ext_extschema SCHEMA has$dollar;
+ERROR:  invalid character in extension "test_ext_extschema" schema: must not contain any of ""$'\"
+CREATE EXTENSION test_ext_extschema SCHEMA "has space";
 --
 -- Test extension with objects outside the extension's schema.
 --
@@ -358,6 +366,11 @@ DROP SCHEMA test_func_dep3;
 --
 -- Test @extschema:extname@ syntax and no_relocate option
 --
+CREATE EXTENSION test_ext_req_schema1 SCHEMA has$dollar;
+CREATE EXTENSION test_ext_req_schema3 CASCADE;
+NOTICE:  installing required extension "test_ext_req_schema2"
+ERROR:  invalid character in extension "test_ext_req_schema1" schema: must not contain any of ""$'\"
+DROP EXTENSION test_ext_req_schema1;
 CREATE SCHEMA test_s_dep;
 CREATE EXTENSION test_ext_req_schema1 SCHEMA test_s_dep;
 CREATE EXTENSION test_ext_req_schema3 CASCADE;
index cbb8401579f2f065c55857c73dc45959d5e5958a..c661afbb9457d40ed973b5ebb264d3e53b1b3639 100644 (file)
@@ -27,6 +27,8 @@ test_install_data += files(
   'test_ext_cyclic1.control',
   'test_ext_cyclic2--1.0.sql',
   'test_ext_cyclic2.control',
+  'test_ext_extschema--1.0.sql',
+  'test_ext_extschema.control',
   'test_ext_evttrig--1.0--2.0.sql',
   'test_ext_evttrig--1.0.sql',
   'test_ext_evttrig.control',
index 5011086183542361846b608927ce12dd065f63ed..51327cc32176c50f5116b59d9996a46181fcbaa1 100644 (file)
@@ -1,18 +1,20 @@
+CREATE SCHEMA has$dollar;
+
 -- test some errors
 CREATE EXTENSION test_ext1;
 CREATE EXTENSION test_ext1 SCHEMA test_ext1;
 CREATE EXTENSION test_ext1 SCHEMA test_ext;
-CREATE SCHEMA test_ext;
-CREATE EXTENSION test_ext1 SCHEMA test_ext;
+CREATE EXTENSION test_ext1 SCHEMA has$dollar;
 
 -- finally success
-CREATE EXTENSION test_ext1 SCHEMA test_ext CASCADE;
+CREATE EXTENSION test_ext1 SCHEMA has$dollar CASCADE;
 
 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;
 
 CREATE EXTENSION test_ext_cyclic1 CASCADE;
 
-DROP SCHEMA test_ext CASCADE;
+DROP SCHEMA has$dollar CASCADE;
+CREATE SCHEMA has$dollar;
 
 CREATE EXTENSION test_ext6;
 DROP EXTENSION test_ext6;
@@ -210,6 +212,13 @@ ALTER EXTENSION test_ext_cine UPDATE TO '1.1';
 
 \dx+ test_ext_cine
 
+--
+-- Test @extschema@ syntax.
+--
+CREATE SCHEMA "has space";
+CREATE EXTENSION test_ext_extschema SCHEMA has$dollar;
+CREATE EXTENSION test_ext_extschema SCHEMA "has space";
+
 --
 -- Test extension with objects outside the extension's schema.
 --
@@ -245,6 +254,9 @@ DROP SCHEMA test_func_dep3;
 --
 -- Test @extschema:extname@ syntax and no_relocate option
 --
+CREATE EXTENSION test_ext_req_schema1 SCHEMA has$dollar;
+CREATE EXTENSION test_ext_req_schema3 CASCADE;
+DROP EXTENSION test_ext_req_schema1;
 CREATE SCHEMA test_s_dep;
 CREATE EXTENSION test_ext_req_schema1 SCHEMA test_s_dep;
 CREATE EXTENSION test_ext_req_schema3 CASCADE;
diff --git a/src/test/modules/test_extensions/test_ext_extschema--1.0.sql b/src/test/modules/test_extensions/test_ext_extschema--1.0.sql
new file mode 100644 (file)
index 0000000..aed5383
--- /dev/null
@@ -0,0 +1,5 @@
+/* src/test/modules/test_extensions/test_ext_extschema--1.0.sql */
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_ext_extschema" to load this file. \quit
+
+SELECT 1 AS @extschema@;
diff --git a/src/test/modules/test_extensions/test_ext_extschema.control b/src/test/modules/test_extensions/test_ext_extschema.control
new file mode 100644 (file)
index 0000000..b124d49
--- /dev/null
@@ -0,0 +1,3 @@
+comment = 'test @extschema@'
+default_version = '1.0'
+relocatable = false