1
0
mirror of https://github.com/postgres/postgres.git synced 2025-06-14 18:42:34 +03:00

Fix column-privilege leak in error-message paths

While building error messages to return to the user,
BuildIndexValueDescription, ExecBuildSlotValueDescription 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
and ExecBuildSlotValueDescription.  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.

Further, in master only, do not return any data for cases where row
security is enabled on the relation and row security should be applied
for the user.  This required a bit of refactoring and moving of things
around related to RLS- note the addition of utils/misc/rls.c.

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.
This commit is contained in:
Stephen Frost
2015-01-12 17:04:11 -05:00
parent acc2b1e843
commit 804b6b6db4
20 changed files with 569 additions and 194 deletions

View File

@ -56,6 +56,7 @@
#include "utils/acl.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/rls.h"
#include "utils/snapmgr.h"
#include "utils/tqual.h"
@ -82,12 +83,23 @@ static void ExecutePlan(EState *estate, PlanState *planstate,
DestReceiver *dest);
static bool ExecCheckRTEPerms(RangeTblEntry *rte);
static void ExecCheckXactReadOnly(PlannedStmt *plannedstmt);
static char *ExecBuildSlotValueDescription(TupleTableSlot *slot,
static char *ExecBuildSlotValueDescription(Oid reloid,
TupleTableSlot *slot,
TupleDesc tupdesc,
Bitmapset *modifiedCols,
int maxfieldlen);
static void EvalPlanQualStart(EPQState *epqstate, EState *parentestate,
Plan *planTree);
/*
* 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 */
@ -1607,15 +1619,24 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
{
if (tupdesc->attrs[attrChk - 1]->attnotnull &&
slot_attisnull(slot, attrChk))
{
char *val_desc;
Bitmapset *modifiedCols;
modifiedCols = GetModifiedColumns(resultRelInfo, estate);
val_desc = ExecBuildSlotValueDescription(RelationGetRelid(rel),
slot,
tupdesc,
modifiedCols,
64);
ereport(ERROR,
(errcode(ERRCODE_NOT_NULL_VIOLATION),
errmsg("null value in column \"%s\" violates not-null constraint",
NameStr(tupdesc->attrs[attrChk - 1]->attname)),
errdetail("Failing row contains %s.",
ExecBuildSlotValueDescription(slot,
tupdesc,
64)),
val_desc ? errdetail("Failing row contains %s.", val_desc) : 0,
errtablecol(rel, attrChk)));
}
}
}
@ -1624,15 +1645,23 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
const char *failed;
if ((failed = ExecRelCheck(resultRelInfo, slot, estate)) != NULL)
{
char *val_desc;
Bitmapset *modifiedCols;
modifiedCols = GetModifiedColumns(resultRelInfo, estate);
val_desc = ExecBuildSlotValueDescription(RelationGetRelid(rel),
slot,
tupdesc,
modifiedCols,
64);
ereport(ERROR,
(errcode(ERRCODE_CHECK_VIOLATION),
errmsg("new row for relation \"%s\" violates check constraint \"%s\"",
RelationGetRelationName(rel), failed),
errdetail("Failing row contains %s.",
ExecBuildSlotValueDescription(slot,
tupdesc,
64)),
val_desc ? errdetail("Failing row contains %s.", val_desc) : 0,
errtableconstraint(rel, failed)));
}
}
}
@ -1643,6 +1672,8 @@ void
ExecWithCheckOptions(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate)
{
Relation rel = resultRelInfo->ri_RelationDesc;
TupleDesc tupdesc = RelationGetDescr(rel);
ExprContext *econtext;
ListCell *l1,
*l2;
@ -1673,14 +1704,24 @@ ExecWithCheckOptions(ResultRelInfo *resultRelInfo,
* case (the opposite of what we do above for CHECK constraints).
*/
if (!ExecQual((List *) wcoExpr, econtext, false))
{
char *val_desc;
Bitmapset *modifiedCols;
modifiedCols = GetModifiedColumns(resultRelInfo, estate);
val_desc = ExecBuildSlotValueDescription(RelationGetRelid(rel),
slot,
tupdesc,
modifiedCols,
64);
ereport(ERROR,
(errcode(ERRCODE_WITH_CHECK_OPTION_VIOLATION),
errmsg("new row violates WITH CHECK OPTION for \"%s\"",
wco->viewname),
errdetail("Failing row contains %s.",
ExecBuildSlotValueDescription(slot,
RelationGetDescr(resultRelInfo->ri_RelationDesc),
64))));
val_desc ? errdetail("Failing row contains %s.", val_desc) :
0));
}
}
}
@ -1696,25 +1737,64 @@ ExecWithCheckOptions(ResultRelInfo *resultRelInfo,
* dropped columns. We used to use the slot's tuple descriptor to decode the
* data, but the slot's descriptor doesn't identify dropped columns, so we
* now need to be passed the relation's descriptor.
*
* Note that, like BuildIndexValueDescription, if the user does not have
* permission to view any of the columns involved, a NULL is returned. Unlike
* BuildIndexValueDescription, if the user has access to view a subset of the
* column involved, that subset will be returned with a key identifying which
* columns they are.
*/
static char *
ExecBuildSlotValueDescription(TupleTableSlot *slot,
ExecBuildSlotValueDescription(Oid reloid,
TupleTableSlot *slot,
TupleDesc tupdesc,
Bitmapset *modifiedCols,
int maxfieldlen)
{
StringInfoData buf;
StringInfoData collist;
bool write_comma = false;
bool write_comma_collist = false;
int i;
AclResult aclresult;
bool table_perm = false;
bool any_perm = false;
/* Make sure the tuple is fully deconstructed */
slot_getallattrs(slot);
/*
* Check if RLS is enabled and should be active for the relation; if so,
* then don't return anything. Otherwise, go through normal permission
* checks.
*/
if (check_enable_rls(reloid, GetUserId(), true) == RLS_ENABLED)
return NULL;
initStringInfo(&buf);
appendStringInfoChar(&buf, '(');
/*
* Check if the user has permissions to see the row. Table-level SELECT
* allows access to all columns. If the user does not have table-level
* SELECT then we check each column and include those the user has SELECT
* rights on. Additionally, we always include columns the user provided
* data for.
*/
aclresult = pg_class_aclcheck(reloid, GetUserId(), ACL_SELECT);
if (aclresult != ACLCHECK_OK)
{
/* Set up the buffer for the column list */
initStringInfo(&collist);
appendStringInfoChar(&collist, '(');
}
else
table_perm = any_perm = true;
/* Make sure the tuple is fully deconstructed */
slot_getallattrs(slot);
for (i = 0; i < tupdesc->natts; i++)
{
bool column_perm = false;
char *val;
int vallen;
@ -1722,37 +1802,76 @@ ExecBuildSlotValueDescription(TupleTableSlot *slot,
if (tupdesc->attrs[i]->attisdropped)
continue;
if (slot->tts_isnull[i])
val = "null";
else
if (!table_perm)
{
Oid foutoid;
bool typisvarlena;
/*
* No table-level SELECT, so need to make sure they either have
* SELECT rights on the column or that they have provided the
* data for the column. If not, omit this column from the error
* message.
*/
aclresult = pg_attribute_aclcheck(reloid, tupdesc->attrs[i]->attnum,
GetUserId(), ACL_SELECT);
if (bms_is_member(tupdesc->attrs[i]->attnum - FirstLowInvalidHeapAttributeNumber,
modifiedCols) || aclresult == ACLCHECK_OK)
{
column_perm = any_perm = true;
getTypeOutputInfo(tupdesc->attrs[i]->atttypid,
&foutoid, &typisvarlena);
val = OidOutputFunctionCall(foutoid, slot->tts_values[i]);
if (write_comma_collist)
appendStringInfoString(&collist, ", ");
else
write_comma_collist = true;
appendStringInfoString(&collist, NameStr(tupdesc->attrs[i]->attname));
}
}
if (write_comma)
appendStringInfoString(&buf, ", ");
else
write_comma = true;
/* truncate if needed */
vallen = strlen(val);
if (vallen <= maxfieldlen)
appendStringInfoString(&buf, val);
else
if (table_perm || column_perm)
{
vallen = pg_mbcliplen(val, vallen, maxfieldlen);
appendBinaryStringInfo(&buf, val, vallen);
appendStringInfoString(&buf, "...");
if (slot->tts_isnull[i])
val = "null";
else
{
Oid foutoid;
bool typisvarlena;
getTypeOutputInfo(tupdesc->attrs[i]->atttypid,
&foutoid, &typisvarlena);
val = OidOutputFunctionCall(foutoid, slot->tts_values[i]);
}
if (write_comma)
appendStringInfoString(&buf, ", ");
else
write_comma = true;
/* truncate if needed */
vallen = strlen(val);
if (vallen <= maxfieldlen)
appendStringInfoString(&buf, val);
else
{
vallen = pg_mbcliplen(val, vallen, maxfieldlen);
appendBinaryStringInfo(&buf, val, vallen);
appendStringInfoString(&buf, "...");
}
}
}
/* If we end up with zero columns being returned, then return NULL. */
if (!any_perm)
return NULL;
appendStringInfoChar(&buf, ')');
if (!table_perm)
{
appendStringInfoString(&collist, ") = ");
appendStringInfoString(&collist, buf.data);
return collist.data;
}
return buf.data;
}