Motivation
==========

SECURITY INVOKER is dangerous, particularly for administrators. There
are numerous ways to put code in a place that's likely to be executed:
triggers, views calling functions, logically-replicated tables, casts,
search path and function resolution tricks, etc. If this code is run
with the privileges of the invoker, then it provides an easy path to
privilege escalation.

We've addressed some of these risks, i.e. by offering better ways to
control the search path, and by ignoring SECURITY INVOKER in some
contexts (like maintenance commands). But it still leaves a lot of
risks for administrators who want to do a SELECT or INSERT. And it
limits major use cases, like logical replication (where the
subscription owner must trust all table owners).

Note that, in the SQL spec, SECURITY DEFINER is the default, which may
be due to some of the dangers of SECURITY INVOKER. (SECURITY DEFINER
carries its own risks, of course, especially if the definer is highly
privileged.)

Prior work
==========

https://www.postgresql.org/message-id/19327.1533748538%40sss.pgh.pa.us

The above thread came up with a couple solutions to express a trust
relationship between users (via GUC or DDL). I'm happy if that
discussion continues, but it appeared to trail off.

My new proposal is different (and simpler, I believe) in two ways:

First, my proposal is only concerned with SECURITY INVOKER functions
and executing arbitrary code written by untrusted users. There's still
the potential for mischief without using SECURITY INVOKER (e.g. if the
search path isn't properly controlled), but it's a different kind of
problem. This narrower problem scope makes my proposal less invasive.

Second, my proposal doesn't establish a new trust relationship. If the
SECURITY INVOKER function is owned by a user that can SET ROLE to you,
you trust it; otherwise not. 

Proposal
========

New boolean GUC check_function_owner_trust, default false.

If check_function_owner_trust=true, throw an error if you try to
execute a function that is SECURITY INVOKER and owned by a user other
than you or someone that can SET ROLE to you.

Use Cases
=========

1. If you are a superuser/admin working on a problem interactively, you
can protect yourself against accidentally executing malicious code with
your privileges.

2. You can set up logical replication subscriptions into tables owned
by users you don't trust, as long as triggers (if needed) can be safely
written as SECURITY DEFINER.

3. You can ensure that running an extension script doesn't somehow
execute malicious code with superuser privileges.

4. Users can protect themselves from executing malicious code in cases
where:
  a. role membership accurately describes the trust relationship
already
  b. triggers, views-calling-UDFs, etc., (if any) can be safely written
as SECURITY DEFINER

While that may not be every conceivable use case, it feels very useful
to me.

Even if you really don't like SECURITY DEFINER, points 1, 3, and 4(a)
seem like wins. And there are a lot of cases where the user simply
doesn't need triggers (etc.).

Extensions
==========

Some extensions might create and extension-specific user that owns lots
of SECURITY INVOKER functions. If this GUC is set, other users wouldn't
be able to call those functions.

Our contrib extensions don't seem do that, and all the tests for them
pass without modification (even when the GUC is true).

For extensions that do create extension-specific users that own
SECURITY INVOKER functions, this GUC alone won't work. Trying to
capture that use case as well could involve more discussion (involving
extension authors) and may result in an extension-specific trust
proposal, so I'm considering that out of scope.

Loose Ends
==========

Do we need to check security-invoker views? I don't think it's nearly
as important, because views can't write data. A security-invoker view
read from a security definer function uses the privileges of the
function owner, so I don't see an obvious way to abuse a security
invoker view, but perhaps I'm not creative enough.

Also, Noah's patch did things differently from mine in a few places,
and I need to work out whether I missed something. I may have to add a
check for the range types "subtype_diff" function, for instance.

Future Work
===========

In some cases, we should consider defaulting (or even forcing) this GUC
to be true, such as in a subscription apply worker.

This proposal may offer a path to allowing non-superusers to create
event triggers.

We may want to provide SECURITY PUBLIC or SECURITY NONE (or even
"SECURITY AS <role>"?), which would execute a function with minimal
privileges, and further reduce the need for executing untrusted
SECURITY INVOKER code.

Another idea is to have READ ONLY functions which would be another way
to make SECURITY INVOKER safer.

-- 
Jeff Davis
PostgreSQL Contributor Team - AWS


From 1b4b575017a226011e6a0174c0a4c372015faa06 Mon Sep 17 00:00:00 2001
From: Jeff Davis <j...@j-davis.com>
Date: Wed, 11 Jan 2023 15:33:47 -0800
Subject: [PATCH v1] Introduce GUC check_function_owner_trust.

If set to true, blocks execution of SECURITY INVOKER functions unless
the function owner can SET ROLE to the current user.
---
 doc/src/sgml/config.sgml                      | 26 ++++++
 src/backend/catalog/aclchk.c                  | 13 +++
 src/backend/optimizer/util/clauses.c          |  2 +
 src/backend/utils/fmgr/fmgr.c                 | 10 +++
 src/backend/utils/misc/guc_tables.c           | 10 +++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/include/utils/acl.h                       |  1 +
 src/include/utils/guc.h                       |  1 +
 src/test/regress/expected/privileges.out      | 85 +++++++++++++++++++
 src/test/regress/sql/privileges.sql           | 57 +++++++++++++
 10 files changed, 206 insertions(+)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 2fec613484..fdfb732a6e 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8707,6 +8707,32 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-check-function-owner-trust" xreflabel="check_function_owner_trust">
+      <term><varname>check_function_owner_trust</varname> (<type>boolean</type>)
+      <indexterm>
+       <primary><varname>check_function_owner_trust</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        This variable controls whether to raise an error in lieu of executing
+        a <literal>SECURITY INVOKER</literal> function (see <xref
+        linkend="sql-createfunction"/>) when that function is owned by an
+        untrusted user. When set to <literal>off</literal> (the default),
+        functions are executed normally regardless of the owner. When set to
+        <literal>on</literal>, an error is raised unless the <literal>SECURITY
+        INVOKER</literal> function is owned by the current user, or a user
+        that can <xref linkend="sql-set-role"/> to the current user.
+       </para>
+
+       <para>
+        Setting this variable to <literal>on</literal> provides protection
+        against unexpectedly executing code written by another user, e.g. due
+        to a trigger (<xref linkend="triggers"/>).
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-default-table-access-method" xreflabel="default_table_access_method">
       <term><varname>default_table_access_method</varname> (<type>string</type>)
       <indexterm>
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index cc6e260908..734c95a3f4 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -3977,6 +3977,19 @@ pg_largeobject_aclcheck_snapshot(Oid lobj_oid, Oid roleid, AclMode mode,
 		return ACLCHECK_NO_PRIV;
 }
 
+/*
+ * For SECURITY INVOKER functions, check that the calling user trusts the
+ * function owner enough to run the code.
+ */
+bool
+function_owner_trust(Oid proowner)
+{
+	if (!check_function_owner_trust)
+		return true;
+
+	return member_can_set_role(proowner, GetUserId());
+}
+
 /*
  * Generic ownership check for an object
  */
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index aa584848cf..9678f55052 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -4412,6 +4412,7 @@ inline_function(Oid funcid, Oid result_type, Oid result_collid,
 		funcform->prokind != PROKIND_FUNCTION ||
 		funcform->prosecdef ||
 		funcform->proretset ||
+		!function_owner_trust(funcform->proowner) ||
 		funcform->prorettype == RECORDOID ||
 		!heap_attisnull(func_tuple, Anum_pg_proc_proconfig, NULL) ||
 		funcform->pronargs != list_length(args))
@@ -4996,6 +4997,7 @@ inline_set_returning_function(PlannerInfo *root, RangeTblEntry *rte)
 		funcform->prorettype == VOIDOID ||
 		funcform->prosecdef ||
 		!funcform->proretset ||
+		!function_owner_trust(funcform->proowner) ||
 		list_length(fexpr->args) != funcform->pronargs ||
 		!heap_attisnull(func_tuple, Anum_pg_proc_proconfig, NULL))
 	{
diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c
index 3f64161760..3985f42290 100644
--- a/src/backend/utils/fmgr/fmgr.c
+++ b/src/backend/utils/fmgr/fmgr.c
@@ -214,6 +214,16 @@ fmgr_info_cxt_security(Oid functionId, FmgrInfo *finfo, MemoryContext mcxt,
 		return;
 	}
 
+	/* SECURITY INVOKER; check that we trust the function owner */
+	if (!ignore_security && !function_owner_trust(procedureStruct->proowner))
+		ereport(ERROR,
+				(errmsg("cannot execute function %s owned by untrusted user %s",
+						NameStr(procedureStruct->proname),
+						GetUserNameFromId(procedureStruct->proowner, false)),
+				 errdetail("When check_function_owner_trust is on, "
+						   "SECURITY INVOKER functions must be owned by "
+						   "a role that can SET ROLE to the calling user.")));
+
 	switch (procedureStruct->prolang)
 	{
 		case INTERNALlanguageId:
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 92545b4958..5a99b66fdc 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -479,6 +479,7 @@ bool		log_btree_build_stats = false;
 char	   *event_source;
 
 bool		row_security;
+bool		check_function_owner_trust = false;
 bool		check_function_bodies = true;
 
 /*
@@ -1576,6 +1577,15 @@ struct config_bool ConfigureNamesBool[] =
 		true,
 		NULL, NULL, NULL
 	},
+	{
+		{"check_function_owner_trust", PGC_USERSET, CLIENT_CONN_STATEMENT,
+			gettext_noop("Check that a SECURITY INVOKER function's owner is trusted before executing."),
+			NULL
+		},
+		&check_function_owner_trust,
+		false,
+		NULL, NULL, NULL
+	},
 	{
 		{"check_function_bodies", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Check routine bodies during CREATE FUNCTION and CREATE PROCEDURE."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index c2ada92054..f37a9c3bba 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -684,6 +684,7 @@
 #default_toast_compression = 'pglz'	# 'pglz' or 'lz4'
 #temp_tablespaces = ''			# a list of tablespace names, '' uses
 					# only default tablespace
+#check_function_owner_trust = off
 #check_function_bodies = on
 #default_transaction_isolation = 'read committed'
 #default_transaction_read_only = off
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index f8e1238fa2..87b775041a 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -258,6 +258,7 @@ extern AclResult pg_parameter_aclcheck(const char *name, Oid roleid,
 									   AclMode mode);
 extern AclResult pg_largeobject_aclcheck_snapshot(Oid lobj_oid, Oid roleid,
 												  AclMode mode, Snapshot snapshot);
+extern bool function_owner_trust(Oid proowner);
 
 extern void aclcheck_error(AclResult aclerr, ObjectType objtype,
 						   const char *objectname);
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index ba89d013e6..cd210f5fcc 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -249,6 +249,7 @@ extern PGDLLIMPORT bool log_executor_stats;
 extern PGDLLIMPORT bool log_statement_stats;
 extern PGDLLIMPORT bool log_btree_build_stats;
 
+extern PGDLLIMPORT bool check_function_owner_trust;
 extern PGDLLIMPORT bool check_function_bodies;
 extern PGDLLIMPORT bool session_auth_is_superuser;
 
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 34c26a804a..5dafbdebb5 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -359,6 +359,91 @@ SELECT * FROM atest1; -- ok
  1 | two
 (2 rows)
 
+-- test that we trust the owner of SECURITY INVOKER functions
+RESET SESSION AUTHORIZATION;
+GRANT regress_priv_user1 TO regress_priv_user2;
+SET SESSION AUTHORIZATION regress_priv_user2;
+CREATE FUNCTION testf_trust1() RETURNS INT SECURITY INVOKER
+  LANGUAGE sql AS $$ SELECT 42 $$;
+CREATE FUNCTION testf_trust2() RETURNS INT SECURITY INVOKER
+  LANGUAGE plpgsql AS $$ BEGIN RETURN 42; END; $$;
+CREATE PROCEDURE testp_trust3() SECURITY INVOKER
+  LANGUAGE plpgsql AS $$ BEGIN PERFORM 42; END; $$;
+CREATE FUNCTION testf_trust4() RETURNS INT SECURITY DEFINER
+  LANGUAGE plpgsql AS $$ BEGIN RETURN 42; END; $$;
+CREATE VIEW testv_trust1 WITH (security_invoker = true)
+  AS SELECT testf_trust4(), testf_trust1();
+CREATE VIEW testv_trust2 WITH (security_invoker = false)
+  AS SELECT testf_trust2(), testf_trust4();
+GRANT SELECT ON testv_trust1 TO regress_priv_user1, regress_priv_user7;
+GRANT SELECT ON testv_trust2 TO regress_priv_user1, regress_priv_user7;
+-- should succeed; regress_priv_user2 is a member of regress_priv_user1
+SET SESSION AUTHORIZATION regress_priv_user1;
+SET check_function_owner_trust = true;
+SELECT testf_trust1();
+ testf_trust1 
+--------------
+           42
+(1 row)
+
+SELECT testf_trust2();
+ testf_trust2 
+--------------
+           42
+(1 row)
+
+CALL testp_trust3();
+SELECT testf_trust4();
+ testf_trust4 
+--------------
+           42
+(1 row)
+
+SELECT * FROM testv_trust1;
+ testf_trust4 | testf_trust1 
+--------------+--------------
+           42 |           42
+(1 row)
+
+SELECT * FROM testv_trust2;
+ testf_trust2 | testf_trust4 
+--------------+--------------
+           42 |           42
+(1 row)
+
+SET SESSION AUTHORIZATION regress_priv_user7;
+SET check_function_owner_trust = true;
+SELECT testf_trust1(); -- fails
+ERROR:  cannot execute function testf_trust1 owned by untrusted user regress_priv_user2
+DETAIL:  When check_function_owner_trust is on, SECURITY INVOKER functions must be owned by a role that can SET ROLE to the calling user.
+SELECT testf_trust2(); -- fails
+ERROR:  cannot execute function testf_trust2 owned by untrusted user regress_priv_user2
+DETAIL:  When check_function_owner_trust is on, SECURITY INVOKER functions must be owned by a role that can SET ROLE to the calling user.
+CALL testp_trust3(); -- fails
+ERROR:  cannot execute function testp_trust3 owned by untrusted user regress_priv_user2
+DETAIL:  When check_function_owner_trust is on, SECURITY INVOKER functions must be owned by a role that can SET ROLE to the calling user.
+SELECT testf_trust4();
+ testf_trust4 
+--------------
+           42
+(1 row)
+
+SELECT * FROM testv_trust1; -- fails
+ERROR:  cannot execute function testf_trust1 owned by untrusted user regress_priv_user2
+DETAIL:  When check_function_owner_trust is on, SECURITY INVOKER functions must be owned by a role that can SET ROLE to the calling user.
+SELECT * FROM testv_trust2; -- fails
+ERROR:  cannot execute function testf_trust2 owned by untrusted user regress_priv_user2
+DETAIL:  When check_function_owner_trust is on, SECURITY INVOKER functions must be owned by a role that can SET ROLE to the calling user.
+SET SESSION AUTHORIZATION regress_priv_user2;
+DROP VIEW testv_trust1;
+DROP VIEW testv_trust2;
+DROP FUNCTION testf_trust1();
+DROP FUNCTION testf_trust2();
+DROP PROCEDURE testp_trust3();
+DROP FUNCTION testf_trust4();
+RESET SESSION AUTHORIZATION;
+REVOKE regress_priv_user1 FROM regress_priv_user2;
+SET check_function_owner_trust TO DEFAULT;
 -- test leaky-function protections in selfuncs
 -- regress_priv_user1 will own a table and provide views for it.
 SET SESSION AUTHORIZATION regress_priv_user1;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 39aa0b4ecf..57fd93a573 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -265,6 +265,63 @@ bar	true
 \.
 SELECT * FROM atest1; -- ok
 
+-- test that we trust the owner of SECURITY INVOKER functions
+
+RESET SESSION AUTHORIZATION;
+GRANT regress_priv_user1 TO regress_priv_user2;
+
+SET SESSION AUTHORIZATION regress_priv_user2;
+
+CREATE FUNCTION testf_trust1() RETURNS INT SECURITY INVOKER
+  LANGUAGE sql AS $$ SELECT 42 $$;
+CREATE FUNCTION testf_trust2() RETURNS INT SECURITY INVOKER
+  LANGUAGE plpgsql AS $$ BEGIN RETURN 42; END; $$;
+CREATE PROCEDURE testp_trust3() SECURITY INVOKER
+  LANGUAGE plpgsql AS $$ BEGIN PERFORM 42; END; $$;
+CREATE FUNCTION testf_trust4() RETURNS INT SECURITY DEFINER
+  LANGUAGE plpgsql AS $$ BEGIN RETURN 42; END; $$;
+
+CREATE VIEW testv_trust1 WITH (security_invoker = true)
+  AS SELECT testf_trust4(), testf_trust1();
+CREATE VIEW testv_trust2 WITH (security_invoker = false)
+  AS SELECT testf_trust2(), testf_trust4();
+
+GRANT SELECT ON testv_trust1 TO regress_priv_user1, regress_priv_user7;
+GRANT SELECT ON testv_trust2 TO regress_priv_user1, regress_priv_user7;
+
+-- should succeed; regress_priv_user2 is a member of regress_priv_user1
+SET SESSION AUTHORIZATION regress_priv_user1;
+SET check_function_owner_trust = true;
+
+SELECT testf_trust1();
+SELECT testf_trust2();
+CALL testp_trust3();
+SELECT testf_trust4();
+SELECT * FROM testv_trust1;
+SELECT * FROM testv_trust2;
+
+SET SESSION AUTHORIZATION regress_priv_user7;
+SET check_function_owner_trust = true;
+
+SELECT testf_trust1(); -- fails
+SELECT testf_trust2(); -- fails
+CALL testp_trust3(); -- fails
+SELECT testf_trust4();
+SELECT * FROM testv_trust1; -- fails
+SELECT * FROM testv_trust2; -- fails
+
+SET SESSION AUTHORIZATION regress_priv_user2;
+DROP VIEW testv_trust1;
+DROP VIEW testv_trust2;
+DROP FUNCTION testf_trust1();
+DROP FUNCTION testf_trust2();
+DROP PROCEDURE testp_trust3();
+DROP FUNCTION testf_trust4();
+
+RESET SESSION AUTHORIZATION;
+REVOKE regress_priv_user1 FROM regress_priv_user2;
+
+SET check_function_owner_trust TO DEFAULT;
 
 -- test leaky-function protections in selfuncs
 
-- 
2.34.1

Reply via email to