]> granicus.if.org Git - postgresql/commitdiff
Fix column-privilege leak in error-message paths
authorStephen Frost <sfrost@snowman.net>
Mon, 12 Jan 2015 22:04:11 +0000 (17:04 -0500)
committerStephen Frost <sfrost@snowman.net>
Wed, 28 Jan 2015 17:33:15 +0000 (12:33 -0500)
While building error messages to return to the user,
BuildIndexValueDescription and ri_ReportViolation would happily include
the entire key or entire row in the result returned to the user, even if
the user didn't have access to view all of the columns being included.

Instead, include only those columns which the user is providing or which
the user has select rights on.  If the user does not have any rights
to view the table or any of the columns involved then no detail is
provided and a NULL value is returned from BuildIndexValueDescription.
Note that, for key cases, the user must have access to all of the
columns for the key to be shown; a partial key will not be returned.

Back-patch all the way, as column-level privileges are now in all
supported versions.

This has been assigned CVE-2014-8161, but since the issue and the patch
have already been publicized on pgsql-hackers, there's no point in trying
to hide this commit.

src/backend/access/index/genam.c
src/backend/access/nbtree/nbtinsert.c
src/backend/commands/copy.c
src/backend/commands/trigger.c
src/backend/executor/execMain.c
src/backend/executor/execUtils.c
src/backend/utils/adt/ri_triggers.c
src/backend/utils/sort/tuplesort.c
src/test/regress/expected/privileges.out
src/test/regress/sql/privileges.sql

index 915dad78d4336ba57a489e92008e70f84a091c7c..b084584f6f1e1bd32f3c9d6a554d607807768e4a 100644 (file)
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "storage/bufmgr.h"
+#include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
+#include "utils/syscache.h"
 #include "utils/tqual.h"
 
 
@@ -150,6 +152,11 @@ IndexScanEnd(IndexScanDesc scan)
  * form "(key_name, ...)=(key_value, ...)".  This is currently used
  * for building unique-constraint and exclusion-constraint error messages.
  *
+ * Note that if the user does not have permissions to view all of the
+ * columns involved then a NULL is returned.  Returning a partial key seems
+ * unlikely to be useful and we have no way to know which of the columns the
+ * user provided.
+ *
  * The passed-in values/nulls arrays are the "raw" input to the index AM,
  * e.g. results of FormIndexDatum --- this is not necessarily what is stored
  * in the index, but it's what the user perceives to be stored.
@@ -159,13 +166,62 @@ BuildIndexValueDescription(Relation indexRelation,
                                                   Datum *values, bool *isnull)
 {
        StringInfoData buf;
+       Form_pg_index idxrec;
+       HeapTuple       ht_idx;
        int                     natts = indexRelation->rd_rel->relnatts;
        int                     i;
+       int                     keyno;
+       Oid                     indexrelid = RelationGetRelid(indexRelation);
+       Oid                     indrelid;
+       AclResult       aclresult;
+
+       /*
+        * Check permissions- if the user does not have access to view all of the
+        * key columns then return NULL to avoid leaking data.
+        *
+        * First we need to check table-level SELECT access and then, if
+        * there is no access there, check column-level permissions.
+        */
+
+       /*
+        * Fetch the pg_index tuple by the Oid of the index
+        */
+       ht_idx = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexrelid));
+       if (!HeapTupleIsValid(ht_idx))
+               elog(ERROR, "cache lookup failed for index %u", indexrelid);
+       idxrec = (Form_pg_index) GETSTRUCT(ht_idx);
+
+       indrelid = idxrec->indrelid;
+       Assert(indexrelid == idxrec->indexrelid);
+
+       /* Table-level SELECT is enough, if the user has it */
+       aclresult = pg_class_aclcheck(indrelid, GetUserId(), ACL_SELECT);
+       if (aclresult != ACLCHECK_OK)
+       {
+               /*
+                * No table-level access, so step through the columns in the
+                * index and make sure the user has SELECT rights on all of them.
+                */
+               for (keyno = 0; keyno < idxrec->indnatts; keyno++)
+               {
+                       AttrNumber      attnum = idxrec->indkey.values[keyno];
+
+                       aclresult = pg_attribute_aclcheck(indrelid, attnum, GetUserId(),
+                                                                                         ACL_SELECT);
+
+                       if (aclresult != ACLCHECK_OK)
+                       {
+                               /* No access, so clean up and return */
+                               ReleaseSysCache(ht_idx);
+                               return NULL;
+                       }
+               }
+       }
+       ReleaseSysCache(ht_idx);
 
        initStringInfo(&buf);
        appendStringInfo(&buf, "(%s)=(",
-                                        pg_get_indexdef_columns(RelationGetRelid(indexRelation),
-                                                                                        true));
+                                        pg_get_indexdef_columns(indexrelid, true));
 
        for (i = 0; i < natts; i++)
        {
index 29e04350c7b10c2da83c41bdb5a24c1cdf4aadf5..b288659d98d0f343f2f15559b4ca7d320d0b0197 100644 (file)
@@ -385,16 +385,20 @@ _bt_check_unique(Relation rel, IndexTuple itup, Relation heapRel,
                                        {
                                                Datum           values[INDEX_MAX_KEYS];
                                                bool            isnull[INDEX_MAX_KEYS];
+                                               char       *key_desc;
 
                                                index_deform_tuple(itup, RelationGetDescr(rel),
                                                                                   values, isnull);
+
+                                               key_desc = BuildIndexValueDescription(rel, values,
+                                                                                                                         isnull);
+
                                                ereport(ERROR,
                                                                (errcode(ERRCODE_UNIQUE_VIOLATION),
                                                                 errmsg("duplicate key value violates unique constraint \"%s\"",
                                                                                RelationGetRelationName(rel)),
-                                                                errdetail("Key %s already exists.",
-                                                                                  BuildIndexValueDescription(rel,
-                                                                                                                 values, isnull))));
+                                                                key_desc ? errdetail("Key %s already exists.",
+                                                                                                         key_desc) : 0));
                                        }
                                }
                                else if (all_dead)
index e91cc7e02c574d801dc05801a4a8068c2c8bf3e0..c75376876409cfb753a725309e767775779bf25b 100644 (file)
@@ -148,6 +148,7 @@ typedef struct CopyStateData
        Oid                *typioparams;        /* array of element types for in_functions */
        int                *defmap;                     /* array of default att numbers */
        ExprState **defexprs;           /* array of default att expressions */
+       List       *range_table;
 
        /*
         * These variables are used to reduce overhead in textual COPY FROM.
@@ -737,6 +738,7 @@ DoCopy(const CopyStmt *stmt, const char *queryString)
        bool            pipe = (stmt->filename == NULL);
        Relation        rel;
        uint64          processed;
+       RangeTblEntry *rte;
 
        /* Disallow file COPY except to superusers. */
        if (!pipe && !superuser())
@@ -750,7 +752,6 @@ DoCopy(const CopyStmt *stmt, const char *queryString)
        {
                TupleDesc       tupDesc;
                AclMode         required_access = (is_from ? ACL_INSERT : ACL_SELECT);
-               RangeTblEntry *rte;
                List       *attnums;
                ListCell   *cur;
 
@@ -795,6 +796,7 @@ DoCopy(const CopyStmt *stmt, const char *queryString)
 
                cstate = BeginCopyFrom(rel, stmt->filename,
                                                           stmt->attlist, stmt->options);
+               cstate->range_table = list_make1(rte);
                processed = CopyFrom(cstate);   /* copy from file to database */
                EndCopyFrom(cstate);
        }
@@ -802,6 +804,7 @@ DoCopy(const CopyStmt *stmt, const char *queryString)
        {
                cstate = BeginCopyTo(rel, stmt->query, queryString, stmt->filename,
                                                         stmt->attlist, stmt->options);
+               cstate->range_table = list_make1(rte);
                processed = DoCopyTo(cstate);   /* copy from database to file */
                EndCopyTo(cstate);
        }
@@ -1933,6 +1936,7 @@ CopyFrom(CopyState cstate)
        estate->es_result_relations = resultRelInfo;
        estate->es_num_result_relations = 1;
        estate->es_result_relation_info = resultRelInfo;
+       estate->es_range_table = cstate->range_table;
 
        /* Set up a tuple slot too */
        myslot = ExecInitExtraTupleSlot(estate);
index bd5dee4286aa91db64015803c1ca32c343532bd4..bbb83911a9a622b1f456a12596da042820f1b996 100644 (file)
 int                    SessionReplicationRole = SESSION_REPLICATION_ROLE_ORIGIN;
 
 
+/*
+ * Note that this macro also exists in executor/execMain.c.  There does not
+ * appear to be any good header to put it into, given the structures that
+ * it uses, so we let them be duplicated.  Be sure to update both if one needs
+ * to be changed, however.
+ */
 #define GetModifiedColumns(relinfo, estate) \
        (rt_fetch((relinfo)->ri_RangeTableIndex, (estate)->es_range_table)->modifiedCols)
 
index cd0def4aab9696905b19d81d642ba69cb5e69002..8ff6ede77146a0e507ba46dde5eee6a5eff3d6d1 100644 (file)
@@ -95,6 +95,15 @@ static void intorel_receive(TupleTableSlot *slot, DestReceiver *self);
 static void intorel_shutdown(DestReceiver *self);
 static void intorel_destroy(DestReceiver *self);
 
+/*
+ * Note that this macro also exists in commands/trigger.c.  There does not
+ * appear to be any good header to put it into, given the structures that
+ * it uses, so we let them be duplicated.  Be sure to update both if one needs
+ * to be changed, however.
+ */
+#define GetModifiedColumns(relinfo, estate) \
+       (rt_fetch((relinfo)->ri_RangeTableIndex, (estate)->es_range_table)->modifiedCols)
+
 /* end of local decls */
 
 
index 23668ef49840aac25370a8f494b8dad3b28c256b..22b48d72bd647174ecf2821d9a40f13d452d26a4 100644 (file)
@@ -1307,15 +1307,19 @@ retry:
                                        (errcode(ERRCODE_EXCLUSION_VIOLATION),
                                         errmsg("could not create exclusion constraint \"%s\"",
                                                        RelationGetRelationName(index)),
-                                        errdetail("Key %s conflicts with key %s.",
-                                                          error_new, error_existing)));
+                                        error_new && error_existing ?
+                                               errdetail("Key %s conflicts with key %s.",
+                                                                 error_new, error_existing) :
+                                               errdetail("Key conflicts exist.")));
                else
                        ereport(ERROR,
                                        (errcode(ERRCODE_EXCLUSION_VIOLATION),
                                         errmsg("conflicting key value violates exclusion constraint \"%s\"",
                                                        RelationGetRelationName(index)),
-                                        errdetail("Key %s conflicts with existing key %s.",
-                                                          error_new, error_existing)));
+                                        error_new && error_existing ?
+                                               errdetail("Key %s conflicts with existing key %s.",
+                                                                 error_new, error_existing) :
+                                               errdetail("Key conflicts with existing key.")));
        }
 
        index_endscan(index_scan);
index 32621e0fc26d0dd8a48d38fd7fe03f0f0288c585..0c1d67e4934f02c545423f9cba2d2824d401786d 100644 (file)
@@ -3495,6 +3495,9 @@ ri_ReportViolation(RI_QueryKey *qkey, const char *constrname,
        bool            onfk;
        int                     idx,
                                key_idx;
+       Oid                     rel_oid;
+       AclResult       aclresult;
+       bool            has_perm = true;
 
        if (spi_err)
                ereport(ERROR,
@@ -3513,12 +3516,14 @@ ri_ReportViolation(RI_QueryKey *qkey, const char *constrname,
        if (onfk)
        {
                key_idx = RI_KEYPAIR_FK_IDX;
+               rel_oid = fk_rel->rd_id;
                if (tupdesc == NULL)
                        tupdesc = fk_rel->rd_att;
        }
        else
        {
                key_idx = RI_KEYPAIR_PK_IDX;
+               rel_oid = pk_rel->rd_id;
                if (tupdesc == NULL)
                        tupdesc = pk_rel->rd_att;
        }
@@ -3538,45 +3543,81 @@ ri_ReportViolation(RI_QueryKey *qkey, const char *constrname,
                                                   RelationGetRelationName(pk_rel))));
        }
 
-       /* Get printable versions of the keys involved */
-       initStringInfo(&key_names);
-       initStringInfo(&key_values);
-       for (idx = 0; idx < qkey->nkeypairs; idx++)
+       /*
+        * Check permissions- if the user does not have access to view the data in
+        * any of the key columns then we don't include the errdetail() below.
+        *
+        * Check table-level permissions first and, failing that, column-level
+        * privileges.
+        */
+       aclresult = pg_class_aclcheck(rel_oid, GetUserId(), ACL_SELECT);
+       if (aclresult != ACLCHECK_OK)
        {
-               int                     fnum = qkey->keypair[idx][key_idx];
-               char       *name,
-                                  *val;
-
-               name = SPI_fname(tupdesc, fnum);
-               val = SPI_getvalue(violator, tupdesc, fnum);
-               if (!val)
-                       val = "null";
+               /* Try for column-level permissions */
+               for (idx = 0; idx < qkey->nkeypairs; idx++)
+               {
+                       aclresult = pg_attribute_aclcheck(rel_oid, qkey->keypair[idx][key_idx],
+                                                                                         GetUserId(),
+                                                                                         ACL_SELECT);
+                       /* No access to the key */
+                       if (aclresult != ACLCHECK_OK)
+                       {
+                               has_perm = false;
+                               break;
+                       }
+               }
+       }
 
-               if (idx > 0)
+       if (has_perm)
+       {
+               /* Get printable versions of the keys involved */
+               initStringInfo(&key_names);
+               initStringInfo(&key_values);
+               for (idx = 0; idx < qkey->nkeypairs; idx++)
                {
-                       appendStringInfoString(&key_names, ", ");
-                       appendStringInfoString(&key_values, ", ");
+                       int                     fnum = qkey->keypair[idx][key_idx];
+                       char       *name,
+                                          *val;
+
+                       name = SPI_fname(tupdesc, fnum);
+                       val = SPI_getvalue(violator, tupdesc, fnum);
+                       if (!val)
+                               val = "null";
+
+                       if (idx > 0)
+                       {
+                               appendStringInfoString(&key_names, ", ");
+                               appendStringInfoString(&key_values, ", ");
+                       }
+                       appendStringInfoString(&key_names, name);
+                       appendStringInfoString(&key_values, val);
                }
-               appendStringInfoString(&key_names, name);
-               appendStringInfoString(&key_values, val);
        }
 
        if (onfk)
                ereport(ERROR,
                                (errcode(ERRCODE_FOREIGN_KEY_VIOLATION),
                                 errmsg("insert or update on table \"%s\" violates foreign key constraint \"%s\"",
-                                               RelationGetRelationName(fk_rel), constrname),
-                                errdetail("Key (%s)=(%s) is not present in table \"%s\".",
-                                                  key_names.data, key_values.data,
-                                                  RelationGetRelationName(pk_rel))));
+                                               RelationGetRelationName(fk_rel),
+                                               constrname),
+                                has_perm ?
+                                        errdetail("Key (%s)=(%s) is not present in table \"%s\".",
+                                                          key_names.data, key_values.data,
+                                                          RelationGetRelationName(pk_rel)) :
+                                        errdetail("Key is not present in table \"%s\".",
+                                                          RelationGetRelationName(pk_rel))));
        else
                ereport(ERROR,
                                (errcode(ERRCODE_FOREIGN_KEY_VIOLATION),
                                 errmsg("update or delete on table \"%s\" violates foreign key constraint \"%s\" on table \"%s\"",
                                                RelationGetRelationName(pk_rel),
-                                               constrname, RelationGetRelationName(fk_rel)),
+                                               constrname,
+                                               RelationGetRelationName(fk_rel)),
+                                has_perm ?
                        errdetail("Key (%s)=(%s) is still referenced from table \"%s\".",
                                          key_names.data, key_values.data,
+                                         RelationGetRelationName(fk_rel)) :
+                                       errdetail("Key is still referenced from table \"%s\".",
                                          RelationGetRelationName(fk_rel))));
 }
 
index 90ef0abca62d0a55d8136cd718ca0f7885a89c8a..b6b06d568560d37506f24297e17bfe969635c82d 100644 (file)
@@ -3124,15 +3124,18 @@ comparetup_index_btree(const SortTuple *a, const SortTuple *b,
        {
                Datum           values[INDEX_MAX_KEYS];
                bool            isnull[INDEX_MAX_KEYS];
+               char       *key_desc;
 
                index_deform_tuple(tuple1, tupDes, values, isnull);
+
+               key_desc = BuildIndexValueDescription(state->indexRel, values, isnull);
+
                ereport(ERROR,
                                (errcode(ERRCODE_UNIQUE_VIOLATION),
                                 errmsg("could not create unique index \"%s\"",
                                                RelationGetRelationName(state->indexRel)),
-                                errdetail("Key %s is duplicated.",
-                                                  BuildIndexValueDescription(state->indexRel,
-                                                                                                         values, isnull))));
+                                key_desc ? errdetail("Key %s is duplicated.", key_desc) :
+                                                       errdetail("Duplicate keys exist.")));
        }
 
        /*
index b3e913de35498d6951bd124bc727d0ba7902675f..dfeb5bde12b856810e2df5cebd47b029add1cd22 100644 (file)
@@ -362,6 +362,34 @@ SELECT atest6 FROM atest6; -- ok
 (0 rows)
 
 COPY atest6 TO stdout; -- ok
+-- check error reporting with column privs
+SET SESSION AUTHORIZATION regressuser1;
+CREATE TABLE t1 (c1 int, c2 int, c3 int check (c3 < 5), primary key (c1, c2));
+NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "t1"
+GRANT SELECT (c1) ON t1 TO regressuser2;
+GRANT INSERT (c1, c2, c3) ON t1 TO regressuser2;
+GRANT UPDATE (c1, c2, c3) ON t1 TO regressuser2;
+-- seed data
+INSERT INTO t1 VALUES (1, 1, 1);
+INSERT INTO t1 VALUES (1, 2, 1);
+INSERT INTO t1 VALUES (2, 1, 2);
+INSERT INTO t1 VALUES (2, 2, 2);
+INSERT INTO t1 VALUES (3, 1, 3);
+SET SESSION AUTHORIZATION regressuser2;
+INSERT INTO t1 (c1, c2) VALUES (1, 1); -- fail, but row not shown
+ERROR:  duplicate key value violates unique constraint "t1_pkey"
+UPDATE t1 SET c2 = 1; -- fail, but row not shown
+ERROR:  duplicate key value violates unique constraint "t1_pkey"
+INSERT INTO t1 (c1, c2) VALUES (null, null); -- fail, but see columns being inserted
+ERROR:  null value in column "c1" violates not-null constraint
+INSERT INTO t1 (c3) VALUES (null); -- fail, but see columns being inserted or have SELECT
+ERROR:  null value in column "c1" violates not-null constraint
+INSERT INTO t1 (c1) VALUES (5); -- fail, but see columns being inserted or have SELECT
+ERROR:  null value in column "c2" violates not-null constraint
+UPDATE t1 SET c3 = 10; -- fail, but see columns with SELECT rights, or being modified
+ERROR:  new row for relation "t1" violates check constraint "t1_c3_check"
+SET SESSION AUTHORIZATION regressuser1;
+DROP TABLE t1;
 -- test column-level privileges when involved with DELETE
 SET SESSION AUTHORIZATION regressuser1;
 ALTER TABLE atest6 ADD COLUMN three integer;
index cc9d85779027a95697a20e00b084bb177397981f..f3cc04bd1d378c45fba9cfecfb408ff18d668b80 100644 (file)
@@ -238,6 +238,31 @@ UPDATE atest5 SET one = 1; -- fail
 SELECT atest6 FROM atest6; -- ok
 COPY atest6 TO stdout; -- ok
 
+-- check error reporting with column privs
+SET SESSION AUTHORIZATION regressuser1;
+CREATE TABLE t1 (c1 int, c2 int, c3 int check (c3 < 5), primary key (c1, c2));
+GRANT SELECT (c1) ON t1 TO regressuser2;
+GRANT INSERT (c1, c2, c3) ON t1 TO regressuser2;
+GRANT UPDATE (c1, c2, c3) ON t1 TO regressuser2;
+
+-- seed data
+INSERT INTO t1 VALUES (1, 1, 1);
+INSERT INTO t1 VALUES (1, 2, 1);
+INSERT INTO t1 VALUES (2, 1, 2);
+INSERT INTO t1 VALUES (2, 2, 2);
+INSERT INTO t1 VALUES (3, 1, 3);
+
+SET SESSION AUTHORIZATION regressuser2;
+INSERT INTO t1 (c1, c2) VALUES (1, 1); -- fail, but row not shown
+UPDATE t1 SET c2 = 1; -- fail, but row not shown
+INSERT INTO t1 (c1, c2) VALUES (null, null); -- fail, but see columns being inserted
+INSERT INTO t1 (c3) VALUES (null); -- fail, but see columns being inserted or have SELECT
+INSERT INTO t1 (c1) VALUES (5); -- fail, but see columns being inserted or have SELECT
+UPDATE t1 SET c3 = 10; -- fail, but see columns with SELECT rights, or being modified
+
+SET SESSION AUTHORIZATION regressuser1;
+DROP TABLE t1;
+
 -- test column-level privileges when involved with DELETE
 SET SESSION AUTHORIZATION regressuser1;
 ALTER TABLE atest6 ADD COLUMN three integer;