aboutsummaryrefslogtreecommitdiff
path: root/src/backend/commands/trigger.c
diff options
context:
space:
mode:
authorTom Lane <tgl@sss.pgh.pa.us>2025-01-23 12:25:45 -0500
committerTom Lane <tgl@sss.pgh.pa.us>2025-01-23 12:25:55 -0500
commit01463e1cccd33fb11b33a4dd6dbebcad3c534102 (patch)
tree2286a844e0c74d727daa74e33068b401d1284864 /src/backend/commands/trigger.c
parent4e7f62bc386a479593e4e8ecfb94370f5a88e522 (diff)
downloadpostgresql-01463e1cccd33fb11b33a4dd6dbebcad3c534102.tar.gz
postgresql-01463e1cccd33fb11b33a4dd6dbebcad3c534102.zip
Ensure that AFTER triggers run as the instigating user.
With deferred triggers, it is possible that the current role changes between the time when the trigger is queued and the time it is executed (for example, the triggering data modification could have been executed in a SECURITY DEFINER function). Up to now, deferred trigger functions would run with the current role set to whatever was active at commit time. That does not matter for foreign-key constraints, whose correctness doesn't depend on the current role. But for user-written triggers, the current role certainly can matter. Hence, fix things so that AFTER triggers are fired under the role that was active when they were queued, matching the behavior of BEFORE triggers which would have actually fired at that time. (If the trigger function is marked SECURITY DEFINER, that of course overrides this, as it always has.) This does not create any new security exposure: if you do DML on a table owned by a hostile user, that user has always had various ways to exploit your permissions, such as the aforementioned BEFORE triggers, default expressions, etc. It might remove some security exposure, because the old behavior could potentially expose some other role besides the one directly modifying the table. There was discussion of making a larger change, such as running as the trigger's owner. However, that would break the common idiom of capturing the value of CURRENT_USER in a trigger for auditing/logging purposes. This change will make no difference in the typical scenario where the current role doesn't change before commit. Arguably this is a bug fix, but it seems too big a semantic change to consider for back-patching. Author: Laurenz Albe <laurenz.albe@cybertec.at> Reviewed-by: Joseph Koshakow <koshy44@gmail.com> Reviewed-by: Pavel Stehule <pavel.stehule@gmail.com> Discussion: https://postgr.es/m/77ee784cf248e842f74588418f55c2931e47bd78.camel@cybertec.at
Diffstat (limited to 'src/backend/commands/trigger.c')
-rw-r--r--src/backend/commands/trigger.c20
1 files changed, 20 insertions, 0 deletions
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 711c09a1030..7a5ffe32f60 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -3635,6 +3635,7 @@ typedef struct AfterTriggerSharedData
TriggerEvent ats_event; /* event type indicator, see trigger.h */
Oid ats_tgoid; /* the trigger's ID */
Oid ats_relid; /* the relation it's on */
+ Oid ats_rolid; /* role to execute the trigger */
CommandId ats_firing_id; /* ID for firing cycle */
struct AfterTriggersTableData *ats_table; /* transition table access */
Bitmapset *ats_modifiedcols; /* modified columns */
@@ -4117,6 +4118,7 @@ afterTriggerAddEvent(AfterTriggerEventList *events,
newshared->ats_firing_id == 0 &&
newshared->ats_table == evtshared->ats_table &&
newshared->ats_relid == evtshared->ats_relid &&
+ newshared->ats_rolid == evtshared->ats_rolid &&
bms_equal(newshared->ats_modifiedcols,
evtshared->ats_modifiedcols))
break;
@@ -4289,6 +4291,8 @@ AfterTriggerExecute(EState *estate,
AfterTriggerShared evtshared = GetTriggerSharedData(event);
Oid tgoid = evtshared->ats_tgoid;
TriggerData LocTriggerData = {0};
+ Oid save_rolid;
+ int save_sec_context;
HeapTuple rettuple;
int tgindx;
bool should_free_trig = false;
@@ -4493,6 +4497,17 @@ AfterTriggerExecute(EState *estate,
MemoryContextReset(per_tuple_context);
/*
+ * If necessary, become the role that was active when the trigger got
+ * queued. Note that the role might have been dropped since the trigger
+ * was queued, but if that is a problem, we will get an error later.
+ * Checking here would still leave a race condition.
+ */
+ GetUserIdAndSecContext(&save_rolid, &save_sec_context);
+ if (save_rolid != evtshared->ats_rolid)
+ SetUserIdAndSecContext(evtshared->ats_rolid,
+ save_sec_context | SECURITY_LOCAL_USERID_CHANGE);
+
+ /*
* Call the trigger and throw away any possibly returned updated tuple.
* (Don't let ExecCallTriggerFunc measure EXPLAIN time.)
*/
@@ -4506,6 +4521,10 @@ AfterTriggerExecute(EState *estate,
rettuple != LocTriggerData.tg_newtuple)
heap_freetuple(rettuple);
+ /* Restore the current role if necessary */
+ if (save_rolid != evtshared->ats_rolid)
+ SetUserIdAndSecContext(save_rolid, save_sec_context);
+
/*
* Release resources
*/
@@ -6431,6 +6450,7 @@ AfterTriggerSaveEvent(EState *estate, ResultRelInfo *relinfo,
(trigger->tginitdeferred ? AFTER_TRIGGER_INITDEFERRED : 0);
new_shared.ats_tgoid = trigger->tgoid;
new_shared.ats_relid = RelationGetRelid(rel);
+ new_shared.ats_rolid = GetUserId();
new_shared.ats_firing_id = 0;
if ((trigger->tgoldtable || trigger->tgnewtable) &&
transition_capture != NULL)