Skip to content

Commit 9406884

Browse files
committed
Fix column-privilege leak in error-message paths
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.
1 parent 8eb1e9d commit 9406884

File tree

10 files changed

+215
-35
lines changed

10 files changed

+215
-35
lines changed

src/backend/access/index/genam.c

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@
2525
#include "miscadmin.h"
2626
#include "pgstat.h"
2727
#include "storage/bufmgr.h"
28+
#include "utils/acl.h"
2829
#include "utils/builtins.h"
2930
#include "utils/lsyscache.h"
3031
#include "utils/rel.h"
32+
#include "utils/syscache.h"
3133
#include "utils/tqual.h"
3234

3335

@@ -150,6 +152,11 @@ IndexScanEnd(IndexScanDesc scan)
150152
* form "(key_name, ...)=(key_value, ...)". This is currently used
151153
* for building unique-constraint and exclusion-constraint error messages.
152154
*
155+
* Note that if the user does not have permissions to view all of the
156+
* columns involved then a NULL is returned. Returning a partial key seems
157+
* unlikely to be useful and we have no way to know which of the columns the
158+
* user provided.
159+
*
153160
* The passed-in values/nulls arrays are the "raw" input to the index AM,
154161
* e.g. results of FormIndexDatum --- this is not necessarily what is stored
155162
* in the index, but it's what the user perceives to be stored.
@@ -159,13 +166,62 @@ BuildIndexValueDescription(Relation indexRelation,
159166
Datum *values, bool *isnull)
160167
{
161168
StringInfoData buf;
169+
Form_pg_index idxrec;
170+
HeapTuple ht_idx;
162171
int natts = indexRelation->rd_rel->relnatts;
163172
int i;
173+
int keyno;
174+
Oid indexrelid = RelationGetRelid(indexRelation);
175+
Oid indrelid;
176+
AclResult aclresult;
177+
178+
/*
179+
* Check permissions- if the user does not have access to view all of the
180+
* key columns then return NULL to avoid leaking data.
181+
*
182+
* First we need to check table-level SELECT access and then, if
183+
* there is no access there, check column-level permissions.
184+
*/
185+
186+
/*
187+
* Fetch the pg_index tuple by the Oid of the index
188+
*/
189+
ht_idx = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexrelid));
190+
if (!HeapTupleIsValid(ht_idx))
191+
elog(ERROR, "cache lookup failed for index %u", indexrelid);
192+
idxrec = (Form_pg_index) GETSTRUCT(ht_idx);
193+
194+
indrelid = idxrec->indrelid;
195+
Assert(indexrelid == idxrec->indexrelid);
196+
197+
/* Table-level SELECT is enough, if the user has it */
198+
aclresult = pg_class_aclcheck(indrelid, GetUserId(), ACL_SELECT);
199+
if (aclresult != ACLCHECK_OK)
200+
{
201+
/*
202+
* No table-level access, so step through the columns in the
203+
* index and make sure the user has SELECT rights on all of them.
204+
*/
205+
for (keyno = 0; keyno < idxrec->indnatts; keyno++)
206+
{
207+
AttrNumber attnum = idxrec->indkey.values[keyno];
208+
209+
aclresult = pg_attribute_aclcheck(indrelid, attnum, GetUserId(),
210+
ACL_SELECT);
211+
212+
if (aclresult != ACLCHECK_OK)
213+
{
214+
/* No access, so clean up and return */
215+
ReleaseSysCache(ht_idx);
216+
return NULL;
217+
}
218+
}
219+
}
220+
ReleaseSysCache(ht_idx);
164221

165222
initStringInfo(&buf);
166223
appendStringInfo(&buf, "(%s)=(",
167-
pg_get_indexdef_columns(RelationGetRelid(indexRelation),
168-
true));
224+
pg_get_indexdef_columns(indexrelid, true));
169225

170226
for (i = 0; i < natts; i++)
171227
{

src/backend/access/nbtree/nbtinsert.c

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -385,16 +385,20 @@ _bt_check_unique(Relation rel, IndexTuple itup, Relation heapRel,
385385
{
386386
Datum values[INDEX_MAX_KEYS];
387387
bool isnull[INDEX_MAX_KEYS];
388+
char *key_desc;
388389

389390
index_deform_tuple(itup, RelationGetDescr(rel),
390391
values, isnull);
392+
393+
key_desc = BuildIndexValueDescription(rel, values,
394+
isnull);
395+
391396
ereport(ERROR,
392397
(errcode(ERRCODE_UNIQUE_VIOLATION),
393398
errmsg("duplicate key value violates unique constraint \"%s\"",
394399
RelationGetRelationName(rel)),
395-
errdetail("Key %s already exists.",
396-
BuildIndexValueDescription(rel,
397-
values, isnull))));
400+
key_desc ? errdetail("Key %s already exists.",
401+
key_desc) : 0));
398402
}
399403
}
400404
else if (all_dead)

src/backend/commands/copy.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ typedef struct CopyStateData
148148
Oid *typioparams; /* array of element types for in_functions */
149149
int *defmap; /* array of default att numbers */
150150
ExprState **defexprs; /* array of default att expressions */
151+
List *range_table;
151152

152153
/*
153154
* These variables are used to reduce overhead in textual COPY FROM.
@@ -737,6 +738,7 @@ DoCopy(const CopyStmt *stmt, const char *queryString)
737738
bool pipe = (stmt->filename == NULL);
738739
Relation rel;
739740
uint64 processed;
741+
RangeTblEntry *rte;
740742

741743
/* Disallow file COPY except to superusers. */
742744
if (!pipe && !superuser())
@@ -750,7 +752,6 @@ DoCopy(const CopyStmt *stmt, const char *queryString)
750752
{
751753
TupleDesc tupDesc;
752754
AclMode required_access = (is_from ? ACL_INSERT : ACL_SELECT);
753-
RangeTblEntry *rte;
754755
List *attnums;
755756
ListCell *cur;
756757

@@ -795,13 +796,15 @@ DoCopy(const CopyStmt *stmt, const char *queryString)
795796

796797
cstate = BeginCopyFrom(rel, stmt->filename,
797798
stmt->attlist, stmt->options);
799+
cstate->range_table = list_make1(rte);
798800
processed = CopyFrom(cstate); /* copy from file to database */
799801
EndCopyFrom(cstate);
800802
}
801803
else
802804
{
803805
cstate = BeginCopyTo(rel, stmt->query, queryString, stmt->filename,
804806
stmt->attlist, stmt->options);
807+
cstate->range_table = list_make1(rte);
805808
processed = DoCopyTo(cstate); /* copy from database to file */
806809
EndCopyTo(cstate);
807810
}
@@ -1933,6 +1936,7 @@ CopyFrom(CopyState cstate)
19331936
estate->es_result_relations = resultRelInfo;
19341937
estate->es_num_result_relations = 1;
19351938
estate->es_result_relation_info = resultRelInfo;
1939+
estate->es_range_table = cstate->range_table;
19361940

19371941
/* Set up a tuple slot too */
19381942
myslot = ExecInitExtraTupleSlot(estate);

src/backend/commands/trigger.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@
6060
int SessionReplicationRole = SESSION_REPLICATION_ROLE_ORIGIN;
6161

6262

63+
/*
64+
* Note that this macro also exists in executor/execMain.c. There does not
65+
* appear to be any good header to put it into, given the structures that
66+
* it uses, so we let them be duplicated. Be sure to update both if one needs
67+
* to be changed, however.
68+
*/
6369
#define GetModifiedColumns(relinfo, estate) \
6470
(rt_fetch((relinfo)->ri_RangeTableIndex, (estate)->es_range_table)->modifiedCols)
6571

src/backend/executor/execMain.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,15 @@ static void intorel_receive(TupleTableSlot *slot, DestReceiver *self);
9595
static void intorel_shutdown(DestReceiver *self);
9696
static void intorel_destroy(DestReceiver *self);
9797

98+
/*
99+
* Note that this macro also exists in commands/trigger.c. There does not
100+
* appear to be any good header to put it into, given the structures that
101+
* it uses, so we let them be duplicated. Be sure to update both if one needs
102+
* to be changed, however.
103+
*/
104+
#define GetModifiedColumns(relinfo, estate) \
105+
(rt_fetch((relinfo)->ri_RangeTableIndex, (estate)->es_range_table)->modifiedCols)
106+
98107
/* end of local decls */
99108

100109

src/backend/executor/execUtils.c

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,15 +1307,19 @@ check_exclusion_constraint(Relation heap, Relation index, IndexInfo *indexInfo,
13071307
(errcode(ERRCODE_EXCLUSION_VIOLATION),
13081308
errmsg("could not create exclusion constraint \"%s\"",
13091309
RelationGetRelationName(index)),
1310-
errdetail("Key %s conflicts with key %s.",
1311-
error_new, error_existing)));
1310+
error_new && error_existing ?
1311+
errdetail("Key %s conflicts with key %s.",
1312+
error_new, error_existing) :
1313+
errdetail("Key conflicts exist.")));
13121314
else
13131315
ereport(ERROR,
13141316
(errcode(ERRCODE_EXCLUSION_VIOLATION),
13151317
errmsg("conflicting key value violates exclusion constraint \"%s\"",
13161318
RelationGetRelationName(index)),
1317-
errdetail("Key %s conflicts with existing key %s.",
1318-
error_new, error_existing)));
1319+
error_new && error_existing ?
1320+
errdetail("Key %s conflicts with existing key %s.",
1321+
error_new, error_existing) :
1322+
errdetail("Key conflicts with existing key.")));
13191323
}
13201324

13211325
index_endscan(index_scan);

src/backend/utils/adt/ri_triggers.c

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3495,6 +3495,9 @@ ri_ReportViolation(RI_QueryKey *qkey, const char *constrname,
34953495
bool onfk;
34963496
int idx,
34973497
key_idx;
3498+
Oid rel_oid;
3499+
AclResult aclresult;
3500+
bool has_perm = true;
34983501

34993502
if (spi_err)
35003503
ereport(ERROR,
@@ -3513,12 +3516,14 @@ ri_ReportViolation(RI_QueryKey *qkey, const char *constrname,
35133516
if (onfk)
35143517
{
35153518
key_idx = RI_KEYPAIR_FK_IDX;
3519+
rel_oid = fk_rel->rd_id;
35163520
if (tupdesc == NULL)
35173521
tupdesc = fk_rel->rd_att;
35183522
}
35193523
else
35203524
{
35213525
key_idx = RI_KEYPAIR_PK_IDX;
3526+
rel_oid = pk_rel->rd_id;
35223527
if (tupdesc == NULL)
35233528
tupdesc = pk_rel->rd_att;
35243529
}
@@ -3538,45 +3543,81 @@ ri_ReportViolation(RI_QueryKey *qkey, const char *constrname,
35383543
RelationGetRelationName(pk_rel))));
35393544
}
35403545

3541-
/* Get printable versions of the keys involved */
3542-
initStringInfo(&key_names);
3543-
initStringInfo(&key_values);
3544-
for (idx = 0; idx < qkey->nkeypairs; idx++)
3546+
/*
3547+
* Check permissions- if the user does not have access to view the data in
3548+
* any of the key columns then we don't include the errdetail() below.
3549+
*
3550+
* Check table-level permissions first and, failing that, column-level
3551+
* privileges.
3552+
*/
3553+
aclresult = pg_class_aclcheck(rel_oid, GetUserId(), ACL_SELECT);
3554+
if (aclresult != ACLCHECK_OK)
35453555
{
3546-
int fnum = qkey->keypair[idx][key_idx];
3547-
char *name,
3548-
*val;
3549-
3550-
name = SPI_fname(tupdesc, fnum);
3551-
val = SPI_getvalue(violator, tupdesc, fnum);
3552-
if (!val)
3553-
val = "null";
3556+
/* Try for column-level permissions */
3557+
for (idx = 0; idx < qkey->nkeypairs; idx++)
3558+
{
3559+
aclresult = pg_attribute_aclcheck(rel_oid, qkey->keypair[idx][key_idx],
3560+
GetUserId(),
3561+
ACL_SELECT);
3562+
/* No access to the key */
3563+
if (aclresult != ACLCHECK_OK)
3564+
{
3565+
has_perm = false;
3566+
break;
3567+
}
3568+
}
3569+
}
35543570

3555-
if (idx > 0)
3571+
if (has_perm)
3572+
{
3573+
/* Get printable versions of the keys involved */
3574+
initStringInfo(&key_names);
3575+
initStringInfo(&key_values);
3576+
for (idx = 0; idx < qkey->nkeypairs; idx++)
35563577
{
3557-
appendStringInfoString(&key_names, ", ");
3558-
appendStringInfoString(&key_values, ", ");
3578+
int fnum = qkey->keypair[idx][key_idx];
3579+
char *name,
3580+
*val;
3581+
3582+
name = SPI_fname(tupdesc, fnum);
3583+
val = SPI_getvalue(violator, tupdesc, fnum);
3584+
if (!val)
3585+
val = "null";
3586+
3587+
if (idx > 0)
3588+
{
3589+
appendStringInfoString(&key_names, ", ");
3590+
appendStringInfoString(&key_values, ", ");
3591+
}
3592+
appendStringInfoString(&key_names, name);
3593+
appendStringInfoString(&key_values, val);
35593594
}
3560-
appendStringInfoString(&key_names, name);
3561-
appendStringInfoString(&key_values, val);
35623595
}
35633596

35643597
if (onfk)
35653598
ereport(ERROR,
35663599
(errcode(ERRCODE_FOREIGN_KEY_VIOLATION),
35673600
errmsg("insert or update on table \"%s\" violates foreign key constraint \"%s\"",
3568-
RelationGetRelationName(fk_rel), constrname),
3569-
errdetail("Key (%s)=(%s) is not present in table \"%s\".",
3570-
key_names.data, key_values.data,
3571-
RelationGetRelationName(pk_rel))));
3601+
RelationGetRelationName(fk_rel),
3602+
constrname),
3603+
has_perm ?
3604+
errdetail("Key (%s)=(%s) is not present in table \"%s\".",
3605+
key_names.data, key_values.data,
3606+
RelationGetRelationName(pk_rel)) :
3607+
errdetail("Key is not present in table \"%s\".",
3608+
RelationGetRelationName(pk_rel))));
35723609
else
35733610
ereport(ERROR,
35743611
(errcode(ERRCODE_FOREIGN_KEY_VIOLATION),
35753612
errmsg("update or delete on table \"%s\" violates foreign key constraint \"%s\" on table \"%s\"",
35763613
RelationGetRelationName(pk_rel),
3577-
constrname, RelationGetRelationName(fk_rel)),
3614+
constrname,
3615+
RelationGetRelationName(fk_rel)),
3616+
has_perm ?
35783617
errdetail("Key (%s)=(%s) is still referenced from table \"%s\".",
35793618
key_names.data, key_values.data,
3619+
RelationGetRelationName(fk_rel)) :
3620+
errdetail("Key is still referenced from table \"%s\".",
35803621
RelationGetRelationName(fk_rel))));
35813622
}
35823623

src/backend/utils/sort/tuplesort.c

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3124,15 +3124,18 @@ comparetup_index_btree(const SortTuple *a, const SortTuple *b,
31243124
{
31253125
Datum values[INDEX_MAX_KEYS];
31263126
bool isnull[INDEX_MAX_KEYS];
3127+
char *key_desc;
31273128

31283129
index_deform_tuple(tuple1, tupDes, values, isnull);
3130+
3131+
key_desc = BuildIndexValueDescription(state->indexRel, values, isnull);
3132+
31293133
ereport(ERROR,
31303134
(errcode(ERRCODE_UNIQUE_VIOLATION),
31313135
errmsg("could not create unique index \"%s\"",
31323136
RelationGetRelationName(state->indexRel)),
3133-
errdetail("Key %s is duplicated.",
3134-
BuildIndexValueDescription(state->indexRel,
3135-
values, isnull))));
3137+
key_desc ? errdetail("Key %s is duplicated.", key_desc) :
3138+
errdetail("Duplicate keys exist.")));
31363139
}
31373140

31383141
/*

src/test/regress/expected/privileges.out

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,34 @@ SELECT atest6 FROM atest6; -- ok
362362
(0 rows)
363363

364364
COPY atest6 TO stdout; -- ok
365+
-- check error reporting with column privs
366+
SET SESSION AUTHORIZATION regressuser1;
367+
CREATE TABLE t1 (c1 int, c2 int, c3 int check (c3 < 5), primary key (c1, c2));
368+
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "t1"
369+
GRANT SELECT (c1) ON t1 TO regressuser2;
370+
GRANT INSERT (c1, c2, c3) ON t1 TO regressuser2;
371+
GRANT UPDATE (c1, c2, c3) ON t1 TO regressuser2;
372+
-- seed data
373+
INSERT INTO t1 VALUES (1, 1, 1);
374+
INSERT INTO t1 VALUES (1, 2, 1);
375+
INSERT INTO t1 VALUES (2, 1, 2);
376+
INSERT INTO t1 VALUES (2, 2, 2);
377+
INSERT INTO t1 VALUES (3, 1, 3);
378+
SET SESSION AUTHORIZATION regressuser2;
379+
INSERT INTO t1 (c1, c2) VALUES (1, 1); -- fail, but row not shown
380+
ERROR: duplicate key value violates unique constraint "t1_pkey"
381+
UPDATE t1 SET c2 = 1; -- fail, but row not shown
382+
ERROR: duplicate key value violates unique constraint "t1_pkey"
383+
INSERT INTO t1 (c1, c2) VALUES (null, null); -- fail, but see columns being inserted
384+
ERROR: null value in column "c1" violates not-null constraint
385+
INSERT INTO t1 (c3) VALUES (null); -- fail, but see columns being inserted or have SELECT
386+
ERROR: null value in column "c1" violates not-null constraint
387+
INSERT INTO t1 (c1) VALUES (5); -- fail, but see columns being inserted or have SELECT
388+
ERROR: null value in column "c2" violates not-null constraint
389+
UPDATE t1 SET c3 = 10; -- fail, but see columns with SELECT rights, or being modified
390+
ERROR: new row for relation "t1" violates check constraint "t1_c3_check"
391+
SET SESSION AUTHORIZATION regressuser1;
392+
DROP TABLE t1;
365393
-- test column-level privileges when involved with DELETE
366394
SET SESSION AUTHORIZATION regressuser1;
367395
ALTER TABLE atest6 ADD COLUMN three integer;

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy