SECURITY

Security

Permissions

Here’s an overview of what users can do depending on the privileges they have:

Action Superuser Owner Masked Role
Create the extension Yes
Drop the extension Yes
Init the extension Yes
Reset the extension Yes
Configure the extension Yes
Put a mask upon a role Yes
Enable dynamic masking Yes
Declare a trusted schema Yes
Declare a trusted function Yes
Create a table Yes Yes
Declare a masking rule Yes Yes
Insert, delete, update a row Yes Yes
Static Masking Yes Yes
Select the real data Yes Yes
Regular Dump Yes Yes
Anonymous Dump Yes Yes
Use the masking functions Yes Yes Yes (1)
Select the masked data Yes Yes Yes
View the masking rules Yes Yes Yes

(1) Except when transparent dynamic masking is enabled: a masked role is then denied any direct call to the RESTRICTED functions, namely anon.pseudo_*(), anon.hash(), anon.digest() and anon.custom_value(). These functions are deterministic, so a masked role able to call them could run them over candidate values and match the result against the masked data it reads, thus recovering the original values.

Limit masking filters only to trusted schemas

By default, the database owner can only write masking rules with functions that are located in the trusted schemas which are controlled by the superusers.

Out of the box, only the anon schema is declared as trusted. This means that by default the functions from the pg_catalog cannot be used in masking rules.

For more details, read the Using pg_catalog functions section.

Timing attacks in LDP functions

This section is intended for maintainers of this extension, not end users.

The GRRM perturbation function (anon.ldp_grrm) decides at runtime whether to keep the original value or replace it with a random lie. A naive implementation using an if/else branch can leak which path was taken through differences in execution time. An attacker who can measure response times precisely enough could use this to figure out whether a particular response is the true value or a perturbed one, which defeats the whole purpose of the differential privacy guarantee.

To prevent this, the function uses branchless bitwise selection: both the true value and the lie value are always computed, and a bitmask is used to pick one of the two without any conditional jump. This makes the execution time constant regardless of which path is taken.

If you modify this function or add new LDP perturbation methods, keep this pattern in mind. Avoid if/else or match on any value that depends on the random keep-or-lie decision. Instead compute both outcomes and select with bitwise operations.

Security context of the functions

Most of the functions of this extension are declared with the SECURITY INVOKER tag. This means that these functions are executed with the privileges of the user that calls them. This is an important restriction.

This extension contains another few functions declared with the tag SECURITY DEFINER.

Hardening principles

You should follow the Principle of Least Privilege when designing your masking policy. To achieve this the user accounts used for static masking should operate with only the minimum permissions necessary to perform data masking.

The TRUSTED schema and function mechanism is trying to do what PostgreSQL is doing at runtime during the declaration phase… with less tools. It’s more reliable and sustainable to rely on PostgreSQL’s ACL to ensure the safety of your data.

The anon.nosuperuser guc is intended to help in the implementation of the Principle of Least Privilege. When it’s set to on, users with the SUPERUSER attribute cannot run:

  • static masking;
  • parallel static masking;
  • dynamic masking;
  • replica masking.

You should use the guc anon.nouperuser for your deployments, as the TRUSTED mechanism is likely be removed in future versions.

Static masking good practices

PostgreSQL keeps a small subset of the data in its system catalogs for both univariate and multivariate statistics such as for most-common-values and histograms.

This data can still be visible after static anonymization has been performed. Usually a substantial amount of data is modified and an autoanalyze will run on the tables. Whether and when this happens depends on the cluster configuration and table size. Furthermore, there is no way to predict when autoanalyze will run.

If a pg_dump --statistics (v18) runs immediately after static anonymization, it could export unmasked data.

Therefore every static masking function attempts to run an ANALYZE command on the target relation at the end of the process. This command requires ownership of the relevant object, the MAINTAIN privilege on it (v17) or membership in the pg_maintain role (v17). If the user lacks the required privilege a warning will be issued and the execution will continue without error.

Example:

-- Before PostgreSQL 17
WARNING:  skipping "hundred" --- only table or database owner can analyze it
-- After PostgreSQL 17
WARNING:  permission denied to analyze "player", skipping it

Unfortunately, there is no easy way to intercept the aforementioned warning from the extension. If the version you are running allows it, grant the appropriate privilege to the anonymization user, otherwise run the ANALYZE command separately with a privileged user.

More generally, it is a good idea to check for table bloat and consider running VACUUM FULL or REPACK where appropriate. For tables that are not rebuilt, running VACUUM FREEZE may help prevent transaction ID wraparound issues and the resulting need for aggressive autovacuum activity later on.

Function with SQL standard body

Functions with SQL-standard bodies undergo parse analysis only at creation time. As a result, transparent dynamic masking would completely miss them. Users subject to transparent dynamic masking are therefore forbidden from executing such functions.

This includes:

  • functions in contrib extensions that use this form (citext, earthdistance, lo, pageinspect, pg_freespacemap, xml2 in 18.6),
  • masking rules such as: MASKED WITH FUNCTION mask.stars(num), where mask.stars() has a RETURN body, whether the function is used directly or through pg_dump.