Skip to content

Commit ff3de4c

Browse files
committed
In security-restricted operations, block enqueue of at-commit user code.
Specifically, this blocks DECLARE ... WITH HOLD and firing of deferred triggers within index expressions and materialized view queries. An attacker having permission to create non-temp objects in at least one schema could execute arbitrary SQL functions under the identity of the bootstrap superuser. One can work around the vulnerability by disabling autovacuum and not manually running ANALYZE, CLUSTER, REINDEX, CREATE INDEX, VACUUM FULL, or REFRESH MATERIALIZED VIEW. (Don't restore from pg_dump, since it runs some of those commands.) Plain VACUUM (without FULL) is safe, and all commands are fine when a trusted user owns the target object. Performance may degrade quickly under this workaround, however. Back-patch to 9.5 (all supported versions). Reviewed by Robert Haas. Reported by Etienne Stalmans. Security: CVE-2020-25695
1 parent d8a9722 commit ff3de4c

File tree

6 files changed

+104
-6
lines changed

6 files changed

+104
-6
lines changed

contrib/postgres_fdw/connection.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,10 @@ pgfdw_report_error(int elevel, PGresult *res, PGconn *conn,
654654

655655
/*
656656
* pgfdw_xact_callback --- cleanup at main-transaction end.
657+
*
658+
* This runs just late enough that it must not enter user-defined code
659+
* locally. (Entering such code on the remote side is fine. Its remote
660+
* COMMIT TRANSACTION may run deferred triggers.)
657661
*/
658662
static void
659663
pgfdw_xact_callback(XactEvent event, void *arg)

src/backend/access/transam/xact.c

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1990,9 +1990,10 @@ CommitTransaction(void)
19901990

19911991
/*
19921992
* Do pre-commit processing that involves calling user-defined code, such
1993-
* as triggers. Since closing cursors could queue trigger actions,
1994-
* triggers could open cursors, etc, we have to keep looping until there's
1995-
* nothing left to do.
1993+
* as triggers. SECURITY_RESTRICTED_OPERATION contexts must not queue an
1994+
* action that would run here, because that would bypass the sandbox.
1995+
* Since closing cursors could queue trigger actions, triggers could open
1996+
* cursors, etc, we have to keep looping until there's nothing left to do.
19961997
*/
19971998
for (;;)
19981999
{
@@ -2010,16 +2011,16 @@ CommitTransaction(void)
20102011
break;
20112012
}
20122013

2013-
CallXactCallbacks(is_parallel_worker ? XACT_EVENT_PARALLEL_PRE_COMMIT
2014-
: XACT_EVENT_PRE_COMMIT);
2015-
20162014
/*
20172015
* The remaining actions cannot call any user-defined code, so it's safe
20182016
* to start shutting down within-transaction services. But note that most
20192017
* of this stuff could still throw an error, which would switch us into
20202018
* the transaction-abort path.
20212019
*/
20222020

2021+
CallXactCallbacks(is_parallel_worker ? XACT_EVENT_PARALLEL_PRE_COMMIT
2022+
: XACT_EVENT_PRE_COMMIT);
2023+
20232024
/* If we might have parallel workers, clean them up now. */
20242025
if (IsInParallelMode())
20252026
AtEOXact_Parallel(true);

src/backend/commands/portalcmds.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include "commands/portalcmds.h"
2828
#include "executor/executor.h"
2929
#include "executor/tstoreReceiver.h"
30+
#include "miscadmin.h"
3031
#include "tcop/pquery.h"
3132
#include "utils/memutils.h"
3233
#include "utils/snapmgr.h"
@@ -67,6 +68,10 @@ PerformCursorOpen(PlannedStmt *stmt, ParamListInfo params,
6768
*/
6869
if (!(cstmt->options & CURSOR_OPT_HOLD))
6970
RequireTransactionChain(isTopLevel, "DECLARE CURSOR");
71+
else if (InSecurityRestrictedOperation())
72+
ereport(ERROR,
73+
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
74+
errmsg("cannot create a cursor WITH HOLD within security-restricted operation")));
7075

7176
/*
7277
* Create a portal and copy the plan and queryString into its memory.

src/backend/commands/trigger.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3701,6 +3701,7 @@ afterTriggerMarkEvents(AfterTriggerEventList *events,
37013701
bool immediate_only)
37023702
{
37033703
bool found = false;
3704+
bool deferred_found = false;
37043705
AfterTriggerEvent event;
37053706
AfterTriggerEventChunk *chunk;
37063707

@@ -3736,13 +3737,24 @@ afterTriggerMarkEvents(AfterTriggerEventList *events,
37363737
*/
37373738
if (defer_it && move_list != NULL)
37383739
{
3740+
deferred_found = true;
37393741
/* add it to move_list */
37403742
afterTriggerAddEvent(move_list, event, evtshared);
37413743
/* mark original copy "done" so we don't do it again */
37423744
event->ate_flags |= AFTER_TRIGGER_DONE;
37433745
}
37443746
}
37453747

3748+
/*
3749+
* We could allow deferred triggers if, before the end of the
3750+
* security-restricted operation, we were to verify that a SET CONSTRAINTS
3751+
* ... IMMEDIATE has fired all such triggers. For now, don't bother.
3752+
*/
3753+
if (deferred_found && InSecurityRestrictedOperation())
3754+
ereport(ERROR,
3755+
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
3756+
errmsg("cannot fire deferred trigger within security-restricted operation")));
3757+
37463758
return found;
37473759
}
37483760

src/test/regress/expected/privileges.out

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1234,6 +1234,48 @@ SELECT has_table_privilege('regress_user1', 'atest4', 'SELECT WITH GRANT OPTION'
12341234
t
12351235
(1 row)
12361236

1237+
-- security-restricted operations
1238+
\c -
1239+
CREATE ROLE regress_sro_user;
1240+
SET SESSION AUTHORIZATION regress_sro_user;
1241+
CREATE FUNCTION unwanted_grant() RETURNS void LANGUAGE sql AS
1242+
'GRANT regress_group2 TO regress_sro_user';
1243+
CREATE FUNCTION mv_action() RETURNS bool LANGUAGE sql AS
1244+
'DECLARE c CURSOR WITH HOLD FOR SELECT unwanted_grant(); SELECT true';
1245+
-- REFRESH of this MV will queue a GRANT at end of transaction
1246+
CREATE MATERIALIZED VIEW sro_mv AS SELECT mv_action() WITH NO DATA;
1247+
REFRESH MATERIALIZED VIEW sro_mv;
1248+
ERROR: cannot create a cursor WITH HOLD within security-restricted operation
1249+
CONTEXT: SQL function "mv_action" statement 1
1250+
\c -
1251+
REFRESH MATERIALIZED VIEW sro_mv;
1252+
ERROR: cannot create a cursor WITH HOLD within security-restricted operation
1253+
CONTEXT: SQL function "mv_action" statement 1
1254+
SET SESSION AUTHORIZATION regress_sro_user;
1255+
-- INSERT to this table will queue a GRANT at end of transaction
1256+
CREATE TABLE sro_trojan_table ();
1257+
CREATE FUNCTION sro_trojan() RETURNS trigger LANGUAGE plpgsql AS
1258+
'BEGIN PERFORM unwanted_grant(); RETURN NULL; END';
1259+
CREATE CONSTRAINT TRIGGER t AFTER INSERT ON sro_trojan_table
1260+
INITIALLY DEFERRED FOR EACH ROW EXECUTE PROCEDURE sro_trojan();
1261+
-- Now, REFRESH will issue such an INSERT, queueing the GRANT
1262+
CREATE OR REPLACE FUNCTION mv_action() RETURNS bool LANGUAGE sql AS
1263+
'INSERT INTO sro_trojan_table DEFAULT VALUES; SELECT true';
1264+
REFRESH MATERIALIZED VIEW sro_mv;
1265+
ERROR: cannot fire deferred trigger within security-restricted operation
1266+
CONTEXT: SQL function "mv_action" statement 1
1267+
\c -
1268+
REFRESH MATERIALIZED VIEW sro_mv;
1269+
ERROR: cannot fire deferred trigger within security-restricted operation
1270+
CONTEXT: SQL function "mv_action" statement 1
1271+
BEGIN; SET CONSTRAINTS ALL IMMEDIATE; REFRESH MATERIALIZED VIEW sro_mv; COMMIT;
1272+
ERROR: must have admin option on role "regress_group2"
1273+
CONTEXT: SQL function "unwanted_grant" statement 1
1274+
SQL statement "SELECT unwanted_grant()"
1275+
PL/pgSQL function sro_trojan() line 1 at PERFORM
1276+
SQL function "mv_action" statement 1
1277+
DROP OWNED BY regress_sro_user;
1278+
DROP ROLE regress_sro_user;
12371279
-- Admin options
12381280
SET SESSION AUTHORIZATION regress_user4;
12391281
CREATE FUNCTION dogrant_ok() RETURNS void LANGUAGE sql SECURITY DEFINER AS

src/test/regress/sql/privileges.sql

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,40 @@ SELECT has_table_privilege('regress_user3', 'atest4', 'SELECT'); -- false
752752
SELECT has_table_privilege('regress_user1', 'atest4', 'SELECT WITH GRANT OPTION'); -- true
753753

754754

755+
-- security-restricted operations
756+
\c -
757+
CREATE ROLE regress_sro_user;
758+
759+
SET SESSION AUTHORIZATION regress_sro_user;
760+
CREATE FUNCTION unwanted_grant() RETURNS void LANGUAGE sql AS
761+
'GRANT regress_group2 TO regress_sro_user';
762+
CREATE FUNCTION mv_action() RETURNS bool LANGUAGE sql AS
763+
'DECLARE c CURSOR WITH HOLD FOR SELECT unwanted_grant(); SELECT true';
764+
-- REFRESH of this MV will queue a GRANT at end of transaction
765+
CREATE MATERIALIZED VIEW sro_mv AS SELECT mv_action() WITH NO DATA;
766+
REFRESH MATERIALIZED VIEW sro_mv;
767+
\c -
768+
REFRESH MATERIALIZED VIEW sro_mv;
769+
770+
SET SESSION AUTHORIZATION regress_sro_user;
771+
-- INSERT to this table will queue a GRANT at end of transaction
772+
CREATE TABLE sro_trojan_table ();
773+
CREATE FUNCTION sro_trojan() RETURNS trigger LANGUAGE plpgsql AS
774+
'BEGIN PERFORM unwanted_grant(); RETURN NULL; END';
775+
CREATE CONSTRAINT TRIGGER t AFTER INSERT ON sro_trojan_table
776+
INITIALLY DEFERRED FOR EACH ROW EXECUTE PROCEDURE sro_trojan();
777+
-- Now, REFRESH will issue such an INSERT, queueing the GRANT
778+
CREATE OR REPLACE FUNCTION mv_action() RETURNS bool LANGUAGE sql AS
779+
'INSERT INTO sro_trojan_table DEFAULT VALUES; SELECT true';
780+
REFRESH MATERIALIZED VIEW sro_mv;
781+
\c -
782+
REFRESH MATERIALIZED VIEW sro_mv;
783+
BEGIN; SET CONSTRAINTS ALL IMMEDIATE; REFRESH MATERIALIZED VIEW sro_mv; COMMIT;
784+
785+
DROP OWNED BY regress_sro_user;
786+
DROP ROLE regress_sro_user;
787+
788+
755789
-- Admin options
756790

757791
SET SESSION AUTHORIZATION regress_user4;

0 commit comments

Comments
 (0)