Blocking execution of SECURITY INVOKER

Started by Jeff Davisabout 3 years ago10 messages
#1Jeff Davis
pgsql@j-davis.com
1 attachment(s)

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
==========

/messages/by-id/19327.1533748538@sss.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

Attachments:

v1-0001-Introduce-GUC-check_function_owner_trust.patchtext/x-patch; charset=UTF-8; name=v1-0001-Introduce-GUC-check_function_owner_trust.patchDownload
From 1b4b575017a226011e6a0174c0a4c372015faa06 Mon Sep 17 00:00:00 2001
From: Jeff Davis <jeff@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

#2Andres Freund
andres@anarazel.de
In reply to: Jeff Davis (#1)
Re: Blocking execution of SECURITY INVOKER

Hi,

On 2023-01-11 18:16:32 -0800, Jeff Davis wrote:

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).

I'm very skeptical about this framing. On the one hand, you can do a lot of
mischief with security definer functions if they get privileged information as
well. But more importantly, just because a function is security definer,
doesn't mean it's safe to be called with attacker controlled input, and the
privilege check will be done with the rights of the admin in many of these
contexts.

And encouraging more security definer functions to be used will cause a lot of
other security issues.

However - I think the concept of more strict ownership checks is a good one. I
just don't think it's right to tie it to SECURITY INVOKER.

I think it'd be quite valuable to have a guc that prevents the execution of
any code that's not directly controlled by the privileged user. Not just
checking function ownership, but also checking ownership of the trigger
definition (i.e. table), check constraints, domain constraints, indexes with
expression columns / partial indexes, etc.

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.

In that case I think what's actually desirable is to simply execute no code
controlled by untrusted users. Even a security definer function can mess up
your day when called in the wrong situation, e.g. due to getting access to the
content of arguments (e.g. a trigger's row contents) or preventing an admin's
write from taking effect (by returning the relevant values from a trigger).

And not ever allowing execution of untrusted code in that situation IME
doesn't prevent desirable things.

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.

I think a much more promising path towards that is to add a feature to logical
replication that changes the execution context to the table owner while
applying those changes.

Using security definer functions for triggers opens up a significant new
attack surface, lots of code that previously didn't need to be safe against
any possible privilege escalation, now needs to be. Expanding the scope of
what needs to protect against privesc, is a BAD idea.

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

It's not safe to allow executing secdef code in that context either. If a less
privileged user manages to get called in that context you don't want to
execute the code, even in a secdef, you want to error out, so the problem can
be detected and rectified.

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

I don't think b) is true very often. And a) seems very problematic as well, as
particularly in "pseudo superuser" environments the most feasible way to
implement pseudo superusers is to automatically grant group membership to the
pseudo superuser. See also e5b8a4c098a.

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.).

4 doesn't seem like a win, it's a requirement?

And as I said, for 1 and 3 I think it's way preferrable to error out.

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.

That'd allow a less-privileged user to completely hobble the admin by erroring
out on all actions.

Greetings,

Andres Freund

#3Jeff Davis
pgsql@j-davis.com
In reply to: Andres Freund (#2)
Re: Blocking execution of SECURITY INVOKER

On Wed, 2023-01-11 at 19:33 -0800, Andres Freund wrote:

and the
privilege check will be done with the rights of the admin in many of
these
contexts.

Can you explain?

And encouraging more security definer functions to be used will cause
a lot of
other security issues.

My proposal just gives some user foo a GUC to say "I am not accepting
the risk of eval()ing whatever arbitrary code finds its way in front of
me with all of my privileges".

If user foo sheds this security burden by setting the GUC, user bar may
then choose to write a trigger function as SECURITY DEFINER so that foo
can access bar's table. But that's the deal the two users struck -- foo
declined the burden, bar accepted it. Why do we want to prevent that
arrangement?

Right now, foo *always* has the burden and no opportunity to decline
it, and even a paranoid user can't figure out what code they will be
executing with a given command. That doesn't seem reasonable to me.

However - I think the concept of more strict ownership checks is a
good one. I
just don't think it's right to tie it to SECURITY INVOKER.

Consider a canonical trigger example like:
https://wiki.postgresql.org/wiki/Audit_trigger or
https://github.com/2ndQuadrant/audit-trigger/blob/master/audit.sql

How can we make that secure for users that insert into the table with
the trigger if you don't differentiate between SECURITY INVOKER and
SECURITY DEFINER? If you allow neither, then it obviously won't work.
And if you allow both, then the owner of the table can change the
function to SECURITY INVOKER and the definition to be malicious a
millisecond before you insert a tuple.

I guess we currently say that anyone foolish enough to insert into a
table that they don't own deserves what they get. That's a weird thing
to say when we have such a fine-grained GRANT system and RLS.

I think it'd be quite valuable to have a guc that prevents the
execution of
any code that's not directly controlled by the privileged user. Not
just
checking function ownership, but also checking ownership of the
trigger
definition (i.e. table), check constraints, domain constraints,
indexes with
expression columns / partial indexes, etc

That sounds like a mix of my proposal and Noah's. The way you've
phrased it seems overly strict though -- do you mean not even execute
untrusted expressions? And it seems to cut out maintenance commands,
which means it would be hard for administrators to use.

I'm OK considering these proposals. Anything that offers some safety.
But it seems like both your proposal and Noah's cut out huge amounts of
functionality unless you have unqualified trust.

Even a security definer function can mess up
your day when called in the wrong situation, e.g. due to getting
access to the
content of arguments (e.g. a trigger's row contents)

I don't see that as a problem. If you're inserting data in a table,
you'd expect the owner of the table to see that data and be able to
modify it as they see fit.

or preventing an admin's
write from taking effect (by returning the relevant values from a
trigger).

I don't see the problem here either. Even if we force the row to be
inserted, the table owner could just delete it.

And not ever allowing execution of untrusted code in that situation
IME
doesn't prevent desirable things.

I don't understand this statement.

I think a much more promising path towards that is to add a feature
to logical
replication that changes the execution context to the table owner
while
applying those changes.

How is that different from SECURITY DEFINER?

And as I said, for 1 and 3 I think it's way preferrable to error out.

My proposal does error out for SECURITY INVOKER, so I suppose you're
saying we should error out for SECURITY DEFINER as well? In the case of
1, I think that would prevent regular maintenance by an admin.

But for use case 3 (extension scripts), I think you're right, erroring
on any non-superuser-owned code is probably good.

--
Jeff Davis
PostgreSQL Contributor Team - AWS

#4Andres Freund
andres@anarazel.de
In reply to: Jeff Davis (#3)
Re: Blocking execution of SECURITY INVOKER

Hi,

On 2023-01-12 18:40:30 -0800, Jeff Davis wrote:

On Wed, 2023-01-11 at 19:33 -0800, Andres Freund wrote:

and the
privilege check will be done with the rights of the admin in many of
these
contexts.

Can you explain?

If the less-privileged user does *not* have execution rights to a security
definer function, but somehow can trick the more-privileged user into calling
the function for them, e.g. by using it as the default expression of a column,
the less-privileged user can escalate to the permissions of the security
definer function.

superuser:
# CREATE FUNCTION exec_su(p_sql text) RETURNS text LANGUAGE plpgsql SECURITY DEFINER AS $$BEGIN RAISE NOTICE 'executing %', p_sql; EXECUTE p_sql;RETURN 'p_sql';END;$$;
# REVOKE ALL ON FUNCTION exec_su FROM PUBLIC ;

unprivileged user:
$ SELECT exec_su('ALTER USER less_privs SUPERUSER');
ERROR: 42501: permission denied for function exec_su
$ CREATE TABLE trick_superuser(value text default exec_su('ALTER USER less_privs SUPERUSER'));

superuser:
# INSERT INTO trick_superuser DEFAULT VALUES;
NOTICE: 00000: executing ALTER USER less_privs SUPERUSER

This case would *not* be prevented by your proposed GUC, unless I miss
something major. The superuser trusts itself and thus the exec_su() function.

And encouraging more security definer functions to be used will cause
a lot of
other security issues.

My proposal just gives some user foo a GUC to say "I am not accepting
the risk of eval()ing whatever arbitrary code finds its way in front of
me with all of my privileges".

If user foo sheds this security burden by setting the GUC, user bar may
then choose to write a trigger function as SECURITY DEFINER so that foo
can access bar's table. But that's the deal the two users struck -- foo
declined the burden, bar accepted it. Why do we want to prevent that
arrangement?

Because it afaict doesn't provide any meaningfully increased security
guarantees (see above), and opens up new ways of attacking, because while
granting execute on a security definer function is low risk, granting execute
on security invoker functions is very high risk, but required for triggers etc
to work.

Right now, foo *always* has the burden and no opportunity to decline
it, and even a paranoid user can't figure out what code they will be
executing with a given command. That doesn't seem reasonable to me.

I agree it's not reasonable - I just don't see the proposal moving the bar.

The proposal to not trust any expressions controlled by untrusted users at
least allows to prevent execution of code, even if it doesn't provide a way to
execute the code in a safe manner. Given that we don't have the former, it
seems foolish to shoot for the latter.

However - I think the concept of more strict ownership checks is a
good one. I
just don't think it's right to tie it to SECURITY INVOKER.

Consider a canonical trigger example like:
https://wiki.postgresql.org/wiki/Audit_trigger or
https://github.com/2ndQuadrant/audit-trigger/blob/master/audit.sql

How can we make that secure for users that insert into the table with
the trigger if you don't differentiate between SECURITY INVOKER and
SECURITY DEFINER? If you allow neither, then it obviously won't work.
And if you allow both, then the owner of the table can change the
function to SECURITY INVOKER and the definition to be malicious a
millisecond before you insert a tuple.

As shown above, triggers are simply not a relevant boundary when a more
privileged user accesses a table controlled by a less privileged user.

And yes, of course an audit function needs to be security definer. But that's
independent of whether it's safe for a more privileged user modify table
contents.

I guess we currently say that anyone foolish enough to insert into a
table that they don't own deserves what they get.

I agree that we have a problem that we should address. I just don't think your
solution works.

That's a weird thing to say when we have such a fine-grained GRANT system
and RLS.

That's a non-sequitur imo. Particularly when RLS you'd not allow
less-privileged users to create any objects, with the possible exception of
temp tables. The point of the grant system is for a privileged user to safely
allow a less privileged user to perform a safe subset of actions. That's just
a separate angle than allowing safe access for a more privileged user to to
objects controlled by a less privileged user.

I think it'd be quite valuable to have a guc that prevents the
execution of
any code that's not directly controlled by the privileged user. Not
just
checking function ownership, but also checking ownership of the
trigger
definition (i.e. table), check constraints, domain constraints,
indexes with
expression columns / partial indexes, etc

That sounds like a mix of my proposal and Noah's. The way you've
phrased it seems overly strict though -- do you mean not even execute
untrusted expressions? And it seems to cut out maintenance commands,
which means it would be hard for administrators to use.

Yes, I mean every expression. As show above, as soon as there is *any*
expression controlled by a less privileged user is executed, the game is lost.

I don't think that prevents all maintenance btw - for things like reindex we
switch to the object owner for evaluation via SetUserIdAndSecContext(). After
checking whether the current user is allowed to that kind of thing.

But for things like default expressions, generated columns etc, I just don't
see an alternative to erroring out when we'd otherwise evaluate an expression
that's controlled by a less privileged user. The admin can alter the table
definition / drop it, if requried.

Even a security definer function can mess up
your day when called in the wrong situation, e.g. due to getting
access to the
content of arguments (e.g. a trigger's row contents)

I don't see that as a problem. If you're inserting data in a table,
you'd expect the owner of the table to see that data and be able to
modify it as they see fit.

or preventing an admin's
write from taking effect (by returning the relevant values from a
trigger).

I don't see the problem here either. Even if we force the row to be
inserted, the table owner could just delete it.

And not ever allowing execution of untrusted code in that situation IME
doesn't prevent desirable things.

I don't understand this statement.

It's not a huge problem if a server admin gets an error while evaluating a
less-privileged-expression, because that's not commonly something that an
admin needs to do. And the admin likely can switch into the user context of
the less privileged user to perform operations in a safer context.

I think a much more promising path towards that is to add a feature to
logical replication that changes the execution context to the table owner
while applying those changes.

How is that different from SECURITY DEFINER?

It protect against vastly more things, see the default expression example.

And as I said, for 1 and 3 I think it's way preferrable to error out.

My proposal does error out for SECURITY INVOKER, so I suppose you're
saying we should error out for SECURITY DEFINER as well? In the case of
1, I think that would prevent regular maintenance by an admin.

What regular maintenance would be prevented? And would it be safe to execute
said code as superuser?

Greetings,

Andres Freund

#5Andres Freund
andres@anarazel.de
In reply to: Andres Freund (#4)
Re: Blocking execution of SECURITY INVOKER

On 2023-01-12 19:29:43 -0800, Andres Freund wrote:

Hi,

On 2023-01-12 18:40:30 -0800, Jeff Davis wrote:

On Wed, 2023-01-11 at 19:33 -0800, Andres Freund wrote:

and the
privilege check will be done with the rights of the admin in many of
these
contexts.

Can you explain?

If the less-privileged user does *not* have execution rights to a security
definer function, but somehow can trick the more-privileged user into calling
the function for them, e.g. by using it as the default expression of a column,
the less-privileged user can escalate to the permissions of the security
definer function.

superuser:
# CREATE FUNCTION exec_su(p_sql text) RETURNS text LANGUAGE plpgsql SECURITY DEFINER AS $$BEGIN RAISE NOTICE 'executing %', p_sql; EXECUTE p_sql;RETURN 'p_sql';END;$$;
# REVOKE ALL ON FUNCTION exec_su FROM PUBLIC ;

unprivileged user:
$ SELECT exec_su('ALTER USER less_privs SUPERUSER');
ERROR: 42501: permission denied for function exec_su
$ CREATE TABLE trick_superuser(value text default exec_su('ALTER USER less_privs SUPERUSER'));

superuser:
# INSERT INTO trick_superuser DEFAULT VALUES;
NOTICE: 00000: executing ALTER USER less_privs SUPERUSER

This case would *not* be prevented by your proposed GUC, unless I miss
something major. The superuser trusts itself and thus the exec_su() function.

Another reason security definer isn't a way to allow safe execution of lesser
privileged code:

superuser (andres):
# CREATE FUNCTION bleat_whoami() RETURNS text LANGUAGE plpgsql SECURITY INVOKER AS $$BEGIN RAISE NOTICE 'whoami: %', current_user;RETURN current_user;END;$$;
# REVOKE ALL ON FUNCTION bleat_whoami FROM PUBLIC;

unprivileged user:
$ CREATE FUNCTION secdef_with_default(foo text = bleat_whoami()) RETURNS text LANGUAGE plpgsql SECURITY DEFINER AS $$BEGIN RETURN 'secdef_with_default';END;$$;
$ SELECT secdef_with_default();
ERROR: 42501: permission denied for function bleat_whoami

superuser (andres):
# SELECT secdef_with_default();
NOTICE: 00000: whoami: andres
LOCATION: exec_stmt_raise, pl_exec.c:3893
┌─────────────────────┐
│ secdef_with_default │
├─────────────────────┤
│ secdef_with_default │
└─────────────────────┘
(1 row)

I.e. the default arguments where evaluated with the invoker's permissions, not
the definer's, despite being controlled by the less privileged user. Worsened
in this case by the fact that this allowed the less privileged user to call a
function they couldn't even call.

Greetings,

Andres Freund

#6Jeff Davis
pgsql@j-davis.com
In reply to: Andres Freund (#4)
Re: Blocking execution of SECURITY INVOKER

Hi,

On Thu, 2023-01-12 at 19:29 -0800, Andres Freund wrote:

superuser:
# CREATE FUNCTION exec_su(p_sql text) RETURNS text LANGUAGE plpgsql
SECURITY DEFINER AS $$BEGIN RAISE NOTICE 'executing %', p_sql;
EXECUTE p_sql;RETURN 'p_sql';END;$$;
# REVOKE ALL ON FUNCTION exec_su FROM PUBLIC ;

That can be solved by creating the function in a schema where ordinary
users don't have USAGE:

CREATE TABLE trick_superuser(value text default admin.exec_su('ALTER
USER less_privs SUPERUSER'));
ERROR: permission denied for schema admin

An interesting case, but it looks more like a gotcha (which is solvable
with best practices); not a fundamental problem.

The point of the grant system is for a privileged user to safely
allow a less privileged user to perform a safe subset of actions.

There is not necessarily a GRANT hierarchy like you describe. The two
users can be peers each with comparable privileges that might make
grants to each other.

And the admin likely can switch into the user context of
the less privileged user to perform operations in a safer context.

How would the admin do that? The malicious UDF can just "RESET SESSION
AUTHORIZATION" to pop back out of the safer context.

If there's not a good way to do this safely now, then we should
probably provide one.

Regards,
--
Jeff Davis
PostgreSQL Contributor Team - AWS

#7Andres Freund
andres@anarazel.de
In reply to: Jeff Davis (#6)
Re: Blocking execution of SECURITY INVOKER

Hi,

On 2023-01-12 23:38:50 -0800, Jeff Davis wrote:

On Thu, 2023-01-12 at 19:29 -0800, Andres Freund wrote:

superuser:
# CREATE FUNCTION exec_su(p_sql text) RETURNS text LANGUAGE plpgsql
SECURITY DEFINER AS $$BEGIN RAISE NOTICE 'executing %', p_sql;
EXECUTE p_sql;RETURN 'p_sql';END;$$;
# REVOKE ALL ON FUNCTION exec_su FROM PUBLIC ;

That can be solved by creating the function in a schema where ordinary
users don't have USAGE:

CREATE TABLE trick_superuser(value text default admin.exec_su('ALTER
USER less_privs SUPERUSER'));
ERROR: permission denied for schema admin

Doubtful. Leaving aside the practicalities of using dedicated schemas and
enforcing their use, there's plenty functions in pg_catalog that a less
privileged user can use to do bad things.

Just think of set_config(), pg_read_file(), lo_create(), binary_upgrade_*(),
pg_drop_replication_slot()...

If the default values get evaluated, this is arbitrary code exec, even if it
requires a few contortions. And the same is true for evaluating *any*
expression.

And the admin likely can switch into the user context of
the less privileged user to perform operations in a safer context.

How would the admin do that? The malicious UDF can just "RESET SESSION
AUTHORIZATION" to pop back out of the safer context.

I thought we had a reasonably convenient way, but now I am not sure
anymore. Might have been via a C helper function. It can be hacked together,
but this is an area that should be as unhacky as possible.

If there's not a good way to do this safely now, then we should
probably provide one.

Yea, particularly because we do have all the infrastructure for it
(c.f. SECURITY_LOCAL_USERID_CHANGE / SECURITY_RESTRICTED_OPERATION).

Greetings,

Andres Freund

#8Jeff Davis
pgsql@j-davis.com
In reply to: Andres Freund (#5)
Re: Blocking execution of SECURITY INVOKER

On Thu, 2023-01-12 at 19:38 -0800, Andres Freund wrote:

I.e. the default arguments where evaluated with the invoker's
permissions, not
the definer's, despite being controlled by the less privileged user.

This is a very interesting case. It also involves tricking the
superuser into executing their own function with the attacker's inputs.
That part is the same as the other case. What's intriguing here is that
it shows the function can be SECURITY INVOKER, and that really means it
could be any builtin function as long as the types work out.

For example:
=> create function trick(l pg_lsn = pg_switch_wal()) returns int
language plpgsql security definer as $$ begin return 42; end; $$;

If the superuser executes that, even though it's a SECURITY DEFINER
function owned by an unprivileged user, it will still call
pg_switch_wal().

--
Jeff Davis
PostgreSQL Contributor Team - AWS

#9Jeff Davis
pgsql@j-davis.com
In reply to: Andres Freund (#7)
Re: Blocking execution of SECURITY INVOKER

On Fri, 2023-01-13 at 00:16 -0800, Andres Freund wrote:

Just think of set_config(), pg_read_file(), lo_create(),
binary_upgrade_*(),
pg_drop_replication_slot()...

Thank you for walking through the details here. I missed it from your
first example because it was an extreme case -- a superuser writing an
eval() security definer function -- so the answer was to lock such a
dangerous function away. But more mild cases are the real problem,
because there's a lot of stuff in pg_catalog.*.

If the default values get evaluated, this is arbitrary code exec,
even if it
requires a few contortions. And the same is true for evaluating *any*
expression.

Right.

However, the normal use case for expressions (whether in a default
expression or check constraint or whatever) is very simple and doesn't
even involve table access. There must be a way to satisfy those simple
cases without opening up a vast attack surface area, and if we do, then
I think my proposal might look useful again.

--
Jeff Davis
PostgreSQL Contributor Team - AWS

#10Andres Freund
andres@anarazel.de
In reply to: Jeff Davis (#9)
Re: Blocking execution of SECURITY INVOKER

Hi,

On 2023-01-13 10:04:19 -0800, Jeff Davis wrote:

However, the normal use case for expressions (whether in a default
expression or check constraint or whatever) is�very simple and doesn't
even involve table access. There must be a way to satisfy those simple
cases without opening up a vast attack surface area, and if we do, then
I think my proposal might look useful again.

I don't see how. I guess we could try to introduce a classification of "never
dangerous" functions (and thus operators). But that seems like a crapton of
work and hard to get right. And I think my examples pretty conclusively show
that security definer isn't a useful boundary to *reduce* privileges. So the
whole idea of preventing only security invoker functions just seems
non-viable.

I think the combination of
a) a setting that restricts evaluation of any non-trusted expressions,
independent of the origin
b) an easy way to execute arbitrary statements within
SECURITY_RESTRICTED_OPERATION

is promising though. In later steps We might be able to use a) to offer the
option to automatically switch to expression owners in specific places (if the
current user has the rights to do that).

An alternative to b would be a version SET ROLE that can't be undone. But I
think we'd just miss all the other things that are prevented by
SECURITY_RESTRICTED_OPERATION.

Greetings,

Andres Freund