pgsql-fix-leaky-view-part-2.patch
application/octet-stream
Filename: pgsql-fix-leaky-view-part-2.patch
Type: application/octet-stream
Part: 0
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: unified
| File | + | − |
|---|---|---|
| doc/src/sgml/ref/create_view.sgml | 21 | 1 |
| doc/src/sgml/rules.sgml | 61 | 0 |
| src/backend/access/common/reloptions.c | 14 | 1 |
| src/backend/commands/tablecmds.c | 2 | 1 |
| src/backend/commands/view.c | 28 | 6 |
| src/backend/nodes/copyfuncs.c | 2 | 0 |
| src/backend/nodes/equalfuncs.c | 2 | 0 |
| src/backend/nodes/outfuncs.c | 2 | 0 |
| src/backend/nodes/readfuncs.c | 2 | 0 |
| src/backend/optimizer/plan/initsplan.c | 75 | 21 |
| src/backend/optimizer/prep/prepjointree.c | 6 | 0 |
| src/backend/optimizer/util/clauses.c | 113 | 0 |
| src/backend/parser/gram.y | 17 | 9 |
| src/backend/rewrite/rewriteHandler.c | 1 | 0 |
| src/backend/utils/cache/lsyscache.c | 19 | 0 |
| src/backend/utils/cache/relcache.c | 1 | 0 |
| src/include/access/reloptions.h | 2 | 1 |
| src/include/nodes/parsenodes.h | 3 | 0 |
| src/include/nodes/primnodes.h | 1 | 0 |
| src/include/optimizer/clauses.h | 1 | 0 |
| src/include/utils/lsyscache.h | 1 | 0 |
| src/include/utils/rel.h | 10 | 1 |
| src/test/regress/expected/create_view.out | 57 | 1 |
| src/test/regress/sql/create_view.sql | 21 | 0 |
doc/src/sgml/ref/create_view.sgml | 22 ++++++-
doc/src/sgml/rules.sgml | 61 ++++++++++++++++
src/backend/access/common/reloptions.c | 15 ++++-
src/backend/commands/tablecmds.c | 3 +-
src/backend/commands/view.c | 34 +++++++--
src/backend/nodes/copyfuncs.c | 2 +
src/backend/nodes/equalfuncs.c | 2 +
src/backend/nodes/outfuncs.c | 2 +
src/backend/nodes/readfuncs.c | 2 +
src/backend/optimizer/plan/initsplan.c | 96 +++++++++++++++++++------
src/backend/optimizer/prep/prepjointree.c | 6 ++
src/backend/optimizer/util/clauses.c | 113 +++++++++++++++++++++++++++++
src/backend/parser/gram.y | 26 +++++---
src/backend/rewrite/rewriteHandler.c | 1 +
src/backend/utils/cache/lsyscache.c | 19 +++++
src/backend/utils/cache/relcache.c | 1 +
src/include/access/reloptions.h | 3 +-
src/include/nodes/parsenodes.h | 3 +
src/include/nodes/primnodes.h | 1 +
src/include/optimizer/clauses.h | 1 +
src/include/utils/lsyscache.h | 1 +
src/include/utils/rel.h | 11 +++-
src/test/regress/expected/create_view.out | 58 +++++++++++++++-
src/test/regress/sql/create_view.sql | 21 ++++++
24 files changed, 462 insertions(+), 42 deletions(-)
diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml
index 417f8c3..a76e646 100644
--- a/doc/src/sgml/ref/create_view.sgml
+++ b/doc/src/sgml/ref/create_view.sgml
@@ -21,7 +21,7 @@ PostgreSQL documentation
<refsynopsisdiv>
<synopsis>
-CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] VIEW <replaceable class="PARAMETER">name</replaceable> [ ( <replaceable class="PARAMETER">column_name</replaceable> [, ...] ) ]
+CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] [SECURITY] VIEW <replaceable class="PARAMETER">name</replaceable> [ ( <replaceable class="PARAMETER">column_name</replaceable> [, ...] ) ]
AS <replaceable class="PARAMETER">query</replaceable>
</synopsis>
</refsynopsisdiv>
@@ -80,6 +80,26 @@ CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] VIEW <replaceable class="PARAMETER">n
</varlistentry>
<varlistentry>
+ <term><literal>SECURITY</literal></term>
+ <listitem>
+ <para>
+ If specified, a part of query optimization shall be restricted
+ to prevent unexpected information leaks.
+ We recommend to apply this option when the view is defined for
+ row-level security purpose, in spite of performance trade-off.
+ </para>
+ <para>
+ It is a commonly-used technique that using views to filter out
+ tuple to be invisible to particular users, however, please note
+ that here is a known-problem that allows malicious users to
+ reference invisible tuples using a function with side-effect
+ because of interaction with query optimization.
+ See <xref linkend="rules-privileges"> for more detailed scenario.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
<term><replaceable class="parameter">name</replaceable></term>
<listitem>
<para>
diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml
index 1b06519..9905044 100644
--- a/doc/src/sgml/rules.sgml
+++ b/doc/src/sgml/rules.sgml
@@ -1856,6 +1856,67 @@ SELECT * FROM phone_number WHERE tricky(person, phone);
</para>
<para>
+ In addition, you might be able to leak contents of invisible tuples
+ using the following scenario:
+<programlisting>
+CREATE VIEW your_credit AS
+ SELECT a.rolname, c.number, c.expire
+ FROM pg_authid a JOIN credit_cards c ON a.oid = c.id
+ WHERE a.rolname = getpgusername();
+</programlisting>
+ This view also might seem secure, since any <command>SELECT</command>
+ from <literal>your_credit</literal> shall be rewritten into a
+ <command>SELECT</command> from the join of <literal>pg_authid</>
+ and <literal>credit_cards</> with a qualifier that filters out
+ any entries except for your credit card number.
+
+ But if a user appends his or her own functions that references
+ only columns come from a particular side of join loop, the optimizer
+ shall relocate this qualifier into the most deep level, independent
+ from cost estimation of the function.
+<programlisting>
+postgres=> SELECT * FROM your_credit WHERE tricky(number, expire);
+NOTICE: 1111-2222-3333-4444 => Jan-01
+NOTICE: 5555-6666-7777-8888 => Feb-02
+NOTICE: 1234-5678-9012-3456 => Mar-03
+ rolname | number | expire
+---------+---------------------+--------
+ alice | 5555-6666-7777-8888 | Feb-02
+(1 row)
+</programlisting>
+ The reason is obvious from the result of <command>EXPLAIN</command>.
+<programlisting>
+postgres=> EXPLAIN SELECT * FROM your_credit WHERE tricky(number, expire);
+ QUERY PLAN
+------------------------------------------------------------------------
+ Hash Join (cost=1.03..20.38 rows=1 width=128)
+ Hash Cond: (c.id = a.oid)
+ -> Seq Scan on credit_cards c (cost=0.00..18.30 rows=277 width=68)
+ Filter: tricky(number, expire)
+ -> Hash (cost=1.01..1.01 rows=1 width=68)
+ -> Seq Scan on pg_authid a (cost=0.00..1.01 rows=1 width=68)
+ Filter: (rolname = getpgusername())
+(7 rows)
+</programlisting>
+ The supplied <function>tricky</function> only references
+ <literal>number</literal> and <literal>expire</literal> columns,
+ however, the qualifier to filter invisible tuples performs on
+ the scan of <literal>pg_authid</literal>.
+ Then, since the optimizer tries to minimize the number of tuples
+ being joined, the supplied qualifer got attached on the scan of
+ <literal>credit_cards</literal>.
+ In the result, it allows <function>tricky</function> to reference
+ contents of the <literal>credit_cards</literal> table.
+</para>
+<para>
+ The <command>CREATE SECURITY VIEW</command> enables to prevent
+ unexpected push-down of qualifiers supplied from outside of the
+ view, however, of course, it is not a fully optimized query plan
+ from the perspective of performance.
+ So, pay attention to the trade-off between security and performance.
+</para>
+
+<para>
Similar considerations apply to update rules. In the examples of
the previous section, the owner of the tables in the example
database could grant the privileges <literal>SELECT</>,
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 4657425..9c3669b 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -66,6 +66,14 @@ static relopt_bool boolRelOpts[] =
},
true
},
+ {
+ {
+ "security_view",
+ "Restrict maximum optimization to prevent data leaks",
+ RELOPT_KIND_VIEW
+ },
+ false
+ },
/* list terminator */
{{NULL}}
};
@@ -776,6 +784,7 @@ extractRelOptions(HeapTuple tuple, TupleDesc tupdesc, Oid amoptions)
{
case RELKIND_RELATION:
case RELKIND_TOASTVALUE:
+ case RELKIND_VIEW:
case RELKIND_UNCATALOGED:
options = heap_reloptions(classForm->relkind, datum, false);
break;
@@ -1134,7 +1143,9 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
{"autovacuum_vacuum_scale_factor", RELOPT_TYPE_REAL,
offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, vacuum_scale_factor)},
{"autovacuum_analyze_scale_factor", RELOPT_TYPE_REAL,
- offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, analyze_scale_factor)}
+ offsetof(StdRdOptions, autovacuum) +offsetof(AutoVacOpts, analyze_scale_factor)},
+ {"security_view", RELOPT_TYPE_BOOL,
+ offsetof(StdRdOptions, security_view)},
};
options = parseRelOptions(reloptions, validate, kind, &numoptions);
@@ -1176,6 +1187,8 @@ heap_reloptions(char relkind, Datum reloptions, bool validate)
return (bytea *) rdopts;
case RELKIND_RELATION:
return default_reloptions(reloptions, validate, RELOPT_KIND_HEAP);
+ case RELKIND_VIEW:
+ return default_reloptions(reloptions, validate, RELOPT_KIND_VIEW);
default:
/* other relkinds are not supported */
return NULL;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index bdbcdff..4649b59 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -2911,7 +2911,7 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
break;
case AT_SetRelOptions: /* SET (...) */
case AT_ResetRelOptions: /* RESET (...) */
- ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX);
+ ATSimplePermissions(rel, ATT_TABLE | ATT_INDEX | ATT_VIEW);
/* This command never recurses */
/* No command-specific prep needed */
pass = AT_PASS_MISC;
@@ -7686,6 +7686,7 @@ ATExecSetRelOptions(Relation rel, List *defList, bool isReset, LOCKMODE lockmode
{
case RELKIND_RELATION:
case RELKIND_TOASTVALUE:
+ case RELKIND_VIEW:
(void) heap_reloptions(rel->rd_rel->relkind, newOptions, true);
break;
case RELKIND_INDEX:
diff --git a/src/backend/commands/view.c b/src/backend/commands/view.c
index be681e3..e1449d1 100644
--- a/src/backend/commands/view.c
+++ b/src/backend/commands/view.c
@@ -97,7 +97,8 @@ isViewOnTempTable_walker(Node *node, void *context)
*---------------------------------------------------------------------
*/
static Oid
-DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace)
+DefineVirtualRelation(const RangeVar *relation, List *tlist,
+ bool replace, bool security)
{
Oid viewOid,
namespaceId;
@@ -167,6 +168,8 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace)
{
Relation rel;
TupleDesc descriptor;
+ List *atcmds = NIL;
+ AlterTableCmd *atcmd;
/*
* Yes. Get exclusive lock on the existing view ...
@@ -205,20 +208,32 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace)
checkViewTupleDesc(descriptor, rel->rd_att);
/*
+ * If "SECURITY" flag is not compatible to the view being replaced,
+ * we need to modify "security_view" reloption here.
+ */
+ if (RelationIsSecurityView(rel) != security)
+ {
+ DefElem *defel = makeDefElem("security_view", NULL);
+
+ atcmd = makeNode(AlterTableCmd);
+ atcmd->subtype = security ? AT_SetRelOptions : AT_ResetRelOptions;
+ atcmd->def = list_make1((Node *) defel);
+
+ atcmds = lappend(atcmds, atcmd);
+ }
+
+ /*
* If new attributes have been added, we must add pg_attribute entries
* for them. It is convenient (although overkill) to use the ALTER
* TABLE ADD COLUMN infrastructure for this.
*/
if (list_length(attrList) > rel->rd_att->natts)
{
- List *atcmds = NIL;
ListCell *c;
int skip = rel->rd_att->natts;
foreach(c, attrList)
{
- AlterTableCmd *atcmd;
-
if (skip > 0)
{
skip--;
@@ -229,8 +244,9 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace)
atcmd->def = (Node *) lfirst(c);
atcmds = lappend(atcmds, atcmd);
}
- AlterTableInternal(viewOid, atcmds, true);
}
+ if (atcmds != NIL)
+ AlterTableInternal(viewOid, atcmds, true);
/*
* Seems okay, so return the OID of the pre-existing view.
@@ -256,6 +272,12 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace)
createStmt->tablespacename = NULL;
createStmt->if_not_exists = false;
+ if (security)
+ {
+ DefElem *defel = makeDefElem("security_view", NULL);
+ createStmt->options = lappend(createStmt->options, defel);
+ }
+
/*
* finally create the relation (this will error out if there's an
* existing view, so we don't need more code to complain if "replace"
@@ -510,7 +532,7 @@ DefineView(ViewStmt *stmt, const char *queryString)
* aborted.
*/
viewOid = DefineVirtualRelation(view, viewParse->targetList,
- stmt->replace);
+ stmt->replace, stmt->security);
/*
* The relation we have just created is not visible to any other commands
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index c9133dd..ce762cc 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1789,6 +1789,7 @@ _copyFromExpr(FromExpr *from)
COPY_NODE_FIELD(fromlist);
COPY_NODE_FIELD(quals);
+ COPY_SCALAR_FIELD(security_view);
return newnode;
}
@@ -1944,6 +1945,7 @@ _copyRangeTblEntry(RangeTblEntry *from)
COPY_SCALAR_FIELD(relid);
COPY_SCALAR_FIELD(relkind);
COPY_NODE_FIELD(subquery);
+ COPY_SCALAR_FIELD(security_view);
COPY_SCALAR_FIELD(jointype);
COPY_NODE_FIELD(joinaliasvars);
COPY_NODE_FIELD(funcexpr);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 3a0267c..89cf310 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -772,6 +772,7 @@ _equalFromExpr(FromExpr *a, FromExpr *b)
{
COMPARE_NODE_FIELD(fromlist);
COMPARE_NODE_FIELD(quals);
+ COMPARE_SCALAR_FIELD(security_view);
return true;
}
@@ -2303,6 +2304,7 @@ _equalRangeTblEntry(RangeTblEntry *a, RangeTblEntry *b)
COMPARE_SCALAR_FIELD(relid);
COMPARE_SCALAR_FIELD(relkind);
COMPARE_NODE_FIELD(subquery);
+ COMPARE_SCALAR_FIELD(security_view);
COMPARE_SCALAR_FIELD(jointype);
COMPARE_NODE_FIELD(joinaliasvars);
COMPARE_NODE_FIELD(funcexpr);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 681f5f8..3ad7067 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1447,6 +1447,7 @@ _outFromExpr(StringInfo str, FromExpr *node)
WRITE_NODE_FIELD(fromlist);
WRITE_NODE_FIELD(quals);
+ WRITE_BOOL_FIELD(security_view);
}
/*****************************************************************************
@@ -2311,6 +2312,7 @@ _outRangeTblEntry(StringInfo str, RangeTblEntry *node)
break;
case RTE_SUBQUERY:
WRITE_NODE_FIELD(subquery);
+ WRITE_BOOL_FIELD(security_view);
break;
case RTE_JOIN:
WRITE_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 2288514..a63236d 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1160,6 +1160,7 @@ _readFromExpr(void)
READ_NODE_FIELD(fromlist);
READ_NODE_FIELD(quals);
+ READ_BOOL_FIELD(security_view);
READ_DONE();
}
@@ -1190,6 +1191,7 @@ _readRangeTblEntry(void)
break;
case RTE_SUBQUERY:
READ_NODE_FIELD(subquery);
+ READ_BOOL_FIELD(security_view);
break;
case RTE_JOIN:
READ_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index 333ede2..628a84e 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -41,7 +41,8 @@ int join_collapse_limit;
static List *deconstruct_recurse(PlannerInfo *root, Node *jtnode,
bool below_outer_join,
- Relids *qualscope, Relids *inner_join_rels);
+ Relids *qualscope, Relids *inner_join_rels,
+ bool below_sec_barriers, Relids *sec_barriers);
static SpecialJoinInfo *make_outerjoininfo(PlannerInfo *root,
Relids left_rels, Relids right_rels,
Relids inner_join_rels,
@@ -52,7 +53,8 @@ static void distribute_qual_to_rels(PlannerInfo *root, Node *clause,
JoinType jointype,
Relids qualscope,
Relids ojscope,
- Relids outerjoin_nonnullable);
+ Relids outerjoin_nonnullable,
+ Relids sec_barriers);
static bool check_outerjoin_delay(PlannerInfo *root, Relids *relids_p,
Relids *nullable_relids_p, bool is_pushed_down);
static bool check_redundant_nullability_qual(PlannerInfo *root, Node *clause);
@@ -240,13 +242,15 @@ deconstruct_jointree(PlannerInfo *root)
{
Relids qualscope;
Relids inner_join_rels;
+ Relids sec_barriers;
+ FromExpr *f = (FromExpr *)root->parse->jointree;
/* Start recursion at top of jointree */
- Assert(root->parse->jointree != NULL &&
- IsA(root->parse->jointree, FromExpr));
+ Assert(root->parse->jointree != NULL && IsA(f, FromExpr));
- return deconstruct_recurse(root, (Node *) root->parse->jointree, false,
- &qualscope, &inner_join_rels);
+ return deconstruct_recurse(root, (Node *) f, false,
+ &qualscope, &inner_join_rels,
+ f->security_view, &sec_barriers);
}
/*
@@ -270,7 +274,8 @@ deconstruct_jointree(PlannerInfo *root)
*/
static List *
deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
- Relids *qualscope, Relids *inner_join_rels)
+ Relids *qualscope, Relids *inner_join_rels,
+ bool below_sec_barriers, Relids *sec_barriers)
{
List *joinlist;
@@ -289,6 +294,9 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
/* A single baserel does not create an inner join */
*inner_join_rels = NULL;
joinlist = list_make1(jtnode);
+ /* Is it in security barrier? */
+ *sec_barriers = (below_sec_barriers ?
+ bms_make_singleton(varno) : NULL);
}
else if (IsA(jtnode, FromExpr))
{
@@ -304,6 +312,7 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
*/
*qualscope = NULL;
*inner_join_rels = NULL;
+ *sec_barriers = NULL;
joinlist = NIL;
remaining = list_length(f->fromlist);
foreach(l, f->fromlist)
@@ -311,12 +320,17 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
Relids sub_qualscope;
List *sub_joinlist;
int sub_members;
+ Relids sub_barriers;
sub_joinlist = deconstruct_recurse(root, lfirst(l),
below_outer_join,
&sub_qualscope,
- inner_join_rels);
+ inner_join_rels,
+ below_sec_barriers ?
+ true : f->security_view,
+ &sub_barriers);
*qualscope = bms_add_members(*qualscope, sub_qualscope);
+ *sec_barriers = bms_add_members(*sec_barriers, sub_barriers);
sub_members = list_length(sub_joinlist);
remaining--;
if (sub_members <= 1 ||
@@ -345,7 +359,7 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
distribute_qual_to_rels(root, qual,
false, below_outer_join, JOIN_INNER,
- *qualscope, NULL, NULL);
+ *qualscope, NULL, NULL, *sec_barriers);
}
}
else if (IsA(jtnode, JoinExpr))
@@ -355,6 +369,8 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
rightids,
left_inners,
right_inners,
+ left_barriers,
+ right_barriers,
nonnullable_rels,
ojscope;
List *leftjoinlist,
@@ -379,12 +395,17 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
case JOIN_INNER:
leftjoinlist = deconstruct_recurse(root, j->larg,
below_outer_join,
- &leftids, &left_inners);
+ &leftids, &left_inners,
+ below_sec_barriers,
+ &left_barriers);
rightjoinlist = deconstruct_recurse(root, j->rarg,
below_outer_join,
- &rightids, &right_inners);
+ &rightids, &right_inners,
+ below_sec_barriers,
+ &right_barriers);
*qualscope = bms_union(leftids, rightids);
*inner_join_rels = *qualscope;
+ *sec_barriers = bms_union(left_barriers, right_barriers);
/* Inner join adds no restrictions for quals */
nonnullable_rels = NULL;
break;
@@ -392,35 +413,50 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
case JOIN_ANTI:
leftjoinlist = deconstruct_recurse(root, j->larg,
below_outer_join,
- &leftids, &left_inners);
+ &leftids, &left_inners,
+ below_sec_barriers,
+ &left_barriers);
rightjoinlist = deconstruct_recurse(root, j->rarg,
true,
- &rightids, &right_inners);
+ &rightids, &right_inners,
+ below_sec_barriers,
+ &right_barriers);
*qualscope = bms_union(leftids, rightids);
*inner_join_rels = bms_union(left_inners, right_inners);
+ *sec_barriers = bms_union(left_barriers, right_barriers);
nonnullable_rels = leftids;
break;
case JOIN_SEMI:
leftjoinlist = deconstruct_recurse(root, j->larg,
below_outer_join,
- &leftids, &left_inners);
+ &leftids, &left_inners,
+ below_sec_barriers,
+ &left_barriers);
rightjoinlist = deconstruct_recurse(root, j->rarg,
below_outer_join,
- &rightids, &right_inners);
+ &rightids, &right_inners,
+ below_sec_barriers,
+ &right_barriers);
*qualscope = bms_union(leftids, rightids);
*inner_join_rels = bms_union(left_inners, right_inners);
+ *sec_barriers = bms_union(left_barriers, right_barriers);
/* Semi join adds no restrictions for quals */
nonnullable_rels = NULL;
break;
case JOIN_FULL:
leftjoinlist = deconstruct_recurse(root, j->larg,
true,
- &leftids, &left_inners);
+ &leftids, &left_inners,
+ below_sec_barriers,
+ &left_barriers);
rightjoinlist = deconstruct_recurse(root, j->rarg,
true,
- &rightids, &right_inners);
+ &rightids, &right_inners,
+ below_sec_barriers,
+ &right_barriers);
*qualscope = bms_union(leftids, rightids);
*inner_join_rels = bms_union(left_inners, right_inners);
+ *sec_barriers = bms_union(left_barriers, right_barriers);
/* each side is both outer and inner */
nonnullable_rels = *qualscope;
break;
@@ -469,7 +505,8 @@ deconstruct_recurse(PlannerInfo *root, Node *jtnode, bool below_outer_join,
distribute_qual_to_rels(root, qual,
false, below_outer_join, j->jointype,
*qualscope,
- ojscope, nonnullable_rels);
+ ojscope, nonnullable_rels,
+ *sec_barriers);
}
/* Now we can add the SpecialJoinInfo to join_info_list */
@@ -793,7 +830,8 @@ distribute_qual_to_rels(PlannerInfo *root, Node *clause,
JoinType jointype,
Relids qualscope,
Relids ojscope,
- Relids outerjoin_nonnullable)
+ Relids outerjoin_nonnullable,
+ Relids sec_barriers)
{
Relids relids;
bool is_pushed_down;
@@ -801,6 +839,7 @@ distribute_qual_to_rels(PlannerInfo *root, Node *clause,
bool pseudoconstant = false;
bool maybe_equivalence;
bool maybe_outer_join;
+ bool maybe_leakable_clause = false;
Relids nullable_relids;
RestrictInfo *restrictinfo;
@@ -873,6 +912,21 @@ distribute_qual_to_rels(PlannerInfo *root, Node *clause,
}
}
+ /*
+ * If and when the supplied clause contains a leakable functions,
+ * it might be used to bypass row-level security using views.
+ * In this case, we should not push down the clause to prevent
+ * the leakable clause being evaluated prior to row-level policy
+ * functions.
+ */
+ if (!bms_is_empty(sec_barriers) &&
+ contain_leakable_functions(clause) &&
+ bms_overlap(relids, sec_barriers))
+ {
+ maybe_leakable_clause = true;
+ relids = bms_add_members(relids, sec_barriers);
+ }
+
/*----------
* Check to see if clause application must be delayed by outer-join
* considerations.
@@ -1075,7 +1129,7 @@ distribute_qual_to_rels(PlannerInfo *root, Node *clause,
* process_equivalence is successful, it will take care of that;
* otherwise, we have to call initialize_mergeclause_eclasses to do it.
*/
- if (restrictinfo->mergeopfamilies)
+ if (!maybe_leakable_clause && restrictinfo->mergeopfamilies)
{
if (maybe_equivalence)
{
@@ -1417,7 +1471,7 @@ process_implied_equality(PlannerInfo *root,
*/
distribute_qual_to_rels(root, (Node *) clause,
true, below_outer_join, JOIN_INNER,
- qualscope, NULL, NULL);
+ qualscope, NULL, NULL, NULL);
}
/*
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index 5d16329..bc23936 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -699,6 +699,12 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte,
pull_up_subqueries(subroot, (Node *) subquery->jointree, NULL, NULL);
/*
+ * If the sub-query is originated from security-view, we mark it here
+ * to prevent too-much optimization in later phase.
+ */
+ ((FromExpr *) subquery->jointree)->security_view = rte->security_view;
+
+ /*
* Now we must recheck whether the subquery is still simple enough to pull
* up. If not, abandon processing it.
*
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 8b0d862..3e0b2e8 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -93,6 +93,7 @@ static bool contain_subplans_walker(Node *node, void *context);
static bool contain_mutable_functions_walker(Node *node, void *context);
static bool contain_volatile_functions_walker(Node *node, void *context);
static bool contain_nonstrict_functions_walker(Node *node, void *context);
+static bool contain_leakable_functions_walker(Node *node, void *context);
static Relids find_nonnullable_rels_walker(Node *node, bool top_level);
static List *find_nonnullable_vars_walker(Node *node, bool top_level);
static bool is_strict_saop(ScalarArrayOpExpr *expr, bool falseOK);
@@ -1164,6 +1165,118 @@ contain_nonstrict_functions_walker(Node *node, void *context)
context);
}
+/*****************************************************************************
+ * Check clauses for leakable functions
+ *****************************************************************************/
+
+/*
+ * contain_leakable_functions
+ * Recursively search for leakable functions within a clause.
+ *
+ * Returns true if any function call with side-effect is found.
+ * ie, some type-input/output handler will raise an error when given
+ * argument does not have a valid format.
+ *
+ * When people uses views for row-level security purpose, given qualifiers
+ * come from outside of the view should not be pushed down into the views,
+ * if they have side-effect, because contents of tuples to be filtered out
+ * may be leaked via side-effectable functions within the qualifiers.
+ *
+ * The idea here is that the planner restrain a part of optimization when
+ * the qualifiers contains leakable functions.
+ * This routine checks whether the given clause contains leakable functions,
+ * or not. If we return false, then the clause is clean.
+ */
+bool
+contain_leakable_functions(Node *clause)
+{
+ return contain_leakable_functions_walker(clause, NULL);
+}
+
+static bool
+contain_leakable_functions_walker(Node *node, void *context)
+{
+ if (node == NULL)
+ return false;
+
+ if (IsA(node, FuncExpr))
+ {
+ /*
+ * currently, we have no way to distinguish a safe function and
+ * a leakable one, so all the function call shall be considered
+ * as leakable one.
+ */
+ return true;
+ }
+ else if (IsA(node, OpExpr))
+ {
+ OpExpr *expr = (OpExpr *) node;
+
+ /*
+ * we assume built-in functions to implement operators are not
+ * leakable, so don't need to prevent optimization.
+ */
+ set_opfuncid(expr);
+ if (get_func_lang(expr->opfuncid) != INTERNALlanguageId)
+ return true;
+ /* else fall through to check args */
+ }
+ else if (IsA(node, DistinctExpr))
+ {
+ DistinctExpr *expr = (DistinctExpr *) node;
+
+ set_opfuncid((OpExpr *) expr);
+ if (get_func_lang(expr->opfuncid) != INTERNALlanguageId)
+ return true;
+ /* else fall through to check args */
+ }
+ else if (IsA(node, ScalarArrayOpExpr))
+ {
+ ScalarArrayOpExpr *expr = (ScalarArrayOpExpr *) node;
+
+ set_sa_opfuncid(expr);
+ if (get_func_lang(expr->opfuncid) != INTERNALlanguageId)
+ return true;
+ /* else fall through to check args */
+ }
+ else if (IsA(node, CoerceViaIO) ||
+ IsA(node, ArrayCoerceExpr))
+ {
+ /*
+ * we assume type-in/out handlers are leakable, even if built-in
+ * functions.
+ * ie, int4in() raises an error message with given argument,
+ * if it does not have valid format for numeric value.
+ */
+ return true;
+ }
+ else if (IsA(node, NullIfExpr))
+ {
+ NullIfExpr *expr = (NullIfExpr *) node;
+
+ set_opfuncid((OpExpr *) expr); /* rely on struct equivalence */
+ if (get_func_lang(expr->opfuncid) != INTERNALlanguageId)
+ return true;
+ /* else fall through to check args */
+ }
+ else if (IsA(node, RowCompareExpr))
+ {
+ /* RowCompare probably can't have volatile ops, but check anyway */
+ RowCompareExpr *rcexpr = (RowCompareExpr *) node;
+ ListCell *opid;
+
+ foreach(opid, rcexpr->opnos)
+ {
+ Oid funcId = get_opcode(lfirst_oid(opid));
+
+ if (get_func_lang(funcId) != INTERNALlanguageId)
+ return true;
+ }
+ /* else fall through to check args */
+ }
+ return expression_tree_walker(node, contain_leakable_functions_walker,
+ context);
+}
/*
* find_nonnullable_rels
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 1d39674..ebe5dff 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -325,6 +325,7 @@ static void SplitColQualList(List *qualList,
%type <boolean> opt_trusted opt_restart_seqs
%type <ival> OptTemp
+%type <boolean> OptSecurity
%type <oncommit> OnCommitOption
%type <node> for_locking_item
@@ -7264,35 +7265,42 @@ transaction_mode_list_or_empty:
/*****************************************************************************
*
* QUERY:
- * CREATE [ OR REPLACE ] [ TEMP ] VIEW <viewname> '('target-list ')'
+ * CREATE [ OR REPLACE ] [ TEMP ] [ SECURITY ]
+ * VIEW <viewname> '('target-list ')'
* AS <query> [ WITH [ CASCADED | LOCAL ] CHECK OPTION ]
*
*****************************************************************************/
-ViewStmt: CREATE OptTemp VIEW qualified_name opt_column_list
+ViewStmt: CREATE OptTemp OptSecurity VIEW qualified_name opt_column_list
AS SelectStmt opt_check_option
{
ViewStmt *n = makeNode(ViewStmt);
- n->view = $4;
+ n->view = $5;
n->view->relpersistence = $2;
- n->aliases = $5;
- n->query = $7;
+ n->aliases = $6;
+ n->query = $8;
n->replace = false;
+ n->security = $3;
$$ = (Node *) n;
}
- | CREATE OR REPLACE OptTemp VIEW qualified_name opt_column_list
+ | CREATE OR REPLACE OptTemp OptSecurity VIEW qualified_name opt_column_list
AS SelectStmt opt_check_option
{
ViewStmt *n = makeNode(ViewStmt);
- n->view = $6;
+ n->view = $7;
n->view->relpersistence = $4;
- n->aliases = $7;
- n->query = $9;
+ n->aliases = $8;
+ n->query = $10;
n->replace = true;
+ n->security = $5;
$$ = (Node *) n;
}
;
+OptSecurity: SECURITY { $$ = TRUE; }
+ | /* EMPTY */ { $$ = FALSE; }
+ ;
+
opt_check_option:
WITH CHECK OPTION
{
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index bfc8fd7..7d25e39 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -1344,6 +1344,7 @@ ApplyRetrieveRule(Query *parsetree,
rte->rtekind = RTE_SUBQUERY;
rte->relid = InvalidOid;
rte->subquery = rule_action;
+ rte->security_view = RelationIsSecurityView(relation);
rte->inh = false; /* must not be set for a subquery */
/*
diff --git a/src/backend/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c
index d3b2a5a..a7e4d3e 100644
--- a/src/backend/utils/cache/lsyscache.c
+++ b/src/backend/utils/cache/lsyscache.c
@@ -1390,6 +1390,25 @@ get_func_namespace(Oid funcid)
}
/*
+ * get_func_lang
+ * Given procedure id, return the function's language
+ */
+Oid
+get_func_lang(Oid funcid)
+{
+ HeapTuple tp;
+ Oid result;
+
+ tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
+ if (!HeapTupleIsValid(tp))
+ elog(ERROR, "cache lookup failed for function %u", funcid);
+
+ result = ((Form_pg_proc) GETSTRUCT(tp))->prolang;
+ ReleaseSysCache(tp);
+ return result;
+}
+
+/*
* get_func_rettype
* Given procedure id, return the function's result type.
*/
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index d7e94ff..ab8146a 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -377,6 +377,7 @@ RelationParseRelOptions(Relation relation, HeapTuple tuple)
case RELKIND_RELATION:
case RELKIND_TOASTVALUE:
case RELKIND_INDEX:
+ case RELKIND_VIEW:
break;
default:
return;
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index c7709cc..586236e 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -42,8 +42,9 @@ typedef enum relopt_kind
RELOPT_KIND_GIST = (1 << 5),
RELOPT_KIND_ATTRIBUTE = (1 << 6),
RELOPT_KIND_TABLESPACE = (1 << 7),
+ RELOPT_KIND_VIEW = (1 << 8),
/* if you add a new kind, make sure you update "last_default" too */
- RELOPT_KIND_LAST_DEFAULT = RELOPT_KIND_TABLESPACE,
+ RELOPT_KIND_LAST_DEFAULT = RELOPT_KIND_VIEW,
/* some compilers treat enums as signed ints, so we can't use 1 << 31 */
RELOPT_KIND_MAX = (1 << 30)
} relopt_kind;
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index ee1881b..e274984 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -706,6 +706,8 @@ typedef struct RangeTblEntry
*/
Query *subquery; /* the sub-query */
+ bool security_view; /* Is the sub-query come from security view? */
+
/*
* Fields valid for a join RTE (else NULL/zero):
*
@@ -2336,6 +2338,7 @@ typedef struct ViewStmt
List *aliases; /* target column names */
Node *query; /* the SELECT query */
bool replace; /* replace an existing view? */
+ bool security; /* view is defined for row-level security? */
} ViewStmt;
/* ----------------------
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index f1e20ef..5e2a788 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1259,6 +1259,7 @@ typedef struct FromExpr
NodeTag type;
List *fromlist; /* List of join subtrees */
Node *quals; /* qualifiers on join, if any */
+ bool security_view; /* It came from security views */
} FromExpr;
#endif /* PRIMNODES_H */
diff --git a/src/include/optimizer/clauses.h b/src/include/optimizer/clauses.h
index 4af772d..035bdf5 100644
--- a/src/include/optimizer/clauses.h
+++ b/src/include/optimizer/clauses.h
@@ -62,6 +62,7 @@ extern bool contain_subplans(Node *clause);
extern bool contain_mutable_functions(Node *clause);
extern bool contain_volatile_functions(Node *clause);
extern bool contain_nonstrict_functions(Node *clause);
+extern bool contain_leakable_functions(Node *clause);
extern Relids find_nonnullable_rels(Node *clause);
extern List *find_nonnullable_vars(Node *clause);
extern List *find_forced_null_vars(Node *clause);
diff --git a/src/include/utils/lsyscache.h b/src/include/utils/lsyscache.h
index 0a419dc..659cdc0 100644
--- a/src/include/utils/lsyscache.h
+++ b/src/include/utils/lsyscache.h
@@ -79,6 +79,7 @@ extern RegProcedure get_oprrest(Oid opno);
extern RegProcedure get_oprjoin(Oid opno);
extern char *get_func_name(Oid funcid);
extern Oid get_func_namespace(Oid funcid);
+extern Oid get_func_lang(Oid funcid);
extern Oid get_func_rettype(Oid funcid);
extern int get_func_nargs(Oid funcid);
extern Oid get_func_signature(Oid funcid, Oid **argtypes, int *nargs);
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index e2c2fa9..3f25949 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -221,7 +221,7 @@ typedef struct RelationData
/*
* StdRdOptions
- * Standard contents of rd_options for heaps and generic indexes.
+ * Standard contents of rd_options for heaps, views and generic indexes.
*
* RelationGetFillFactor() and RelationGetTargetPageFreeSpace() can only
* be applied to relations that use this format or a superset for
@@ -247,6 +247,7 @@ typedef struct StdRdOptions
int32 vl_len_; /* varlena header (do not touch directly!) */
int fillfactor; /* page fill factor in percent (0..100) */
AutoVacOpts autovacuum; /* autovacuum-related options */
+ bool security_view; /* restrict fully optimization, if true */
} StdRdOptions;
#define HEAP_MIN_FILLFACTOR 10
@@ -275,6 +276,14 @@ typedef struct StdRdOptions
(BLCKSZ * (100 - RelationGetFillFactor(relation, defaultff)) / 100)
/*
+ * RelationIsSecurityView
+ * Returns whether the relation is security view, or not
+ */
+#define RelationIsSecurityView(relation) \
+ ((relation)->rd_options ? \
+ ((StdRdOptions *) (relation)->rd_options)->security_view : false)
+
+/*
* RelationIsValid
* True iff relation descriptor is valid.
*/
diff --git a/src/test/regress/expected/create_view.out b/src/test/regress/expected/create_view.out
index f2c0685..ceaa701 100644
--- a/src/test/regress/expected/create_view.out
+++ b/src/test/regress/expected/create_view.out
@@ -239,6 +239,58 @@ And relnamespace IN (SELECT OID FROM pg_namespace WHERE nspname LIKE 'pg_temp%')
1
(1 row)
+--Should work correctly to leaky-view scenario
+CREATE TABLE lvtest1 (a int, b text);
+CREATE TABLE lvtest2 (x int, y text);
+CREATE FUNCTION f_leak(text) RETURNS bool LANGUAGE 'plpgsql'
+AS 'BEGIN raise notice ''leak => %'', $1; RETURN true; END';
+INSERT INTO lvtest1 VALUES (10, 'aaa'), (11, 'bbb'), (12, 'ccc'), (13, 'ddd');
+INSERT INTO lvtest2 VALUES (11, 'xxx'), (12, 'yyy'), (13, 'zzz'), (14, 'xyz');
+CREATE OR REPLACE VIEW leaky_view AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 0;
+SELECT * FROM leaky_view WHERE f_leak(y);
+NOTICE: leak => xxx
+NOTICE: leak => yyy
+NOTICE: leak => zzz
+NOTICE: leak => xyz
+ a | b | x | y
+----+-----+----+-----
+ 12 | ccc | 12 | yyy
+(1 row)
+
+EXPLAIN SELECT * FROM leaky_view WHERE f_leak(y);
+ QUERY PLAN
+---------------------------------------------------------------------
+ Hash Join (cost=28.52..359.98 rows=12 width=72)
+ Hash Cond: (lvtest2.x = lvtest1.a)
+ -> Seq Scan on lvtest2 (cost=0.00..329.80 rows=410 width=36)
+ Filter: f_leak(y)
+ -> Hash (cost=28.45..28.45 rows=6 width=36)
+ -> Seq Scan on lvtest1 (cost=0.00..28.45 rows=6 width=36)
+ Filter: ((a % 2) = 0)
+(7 rows)
+
+CREATE OR REPLACE SECURITY VIEW leaky_view AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 0;
+SELECT * FROM leaky_view WHERE f_leak(y);
+NOTICE: leak => yyy
+ a | b | x | y
+----+-----+----+-----
+ 12 | ccc | 12 | yyy
+(1 row)
+
+EXPLAIN SELECT * FROM leaky_view WHERE f_leak(y);
+ QUERY PLAN
+---------------------------------------------------------------------
+ Hash Join (cost=28.52..65.06 rows=12 width=72)
+ Hash Cond: (lvtest2.x = lvtest1.a)
+ Join Filter: f_leak(lvtest2.y)
+ -> Seq Scan on lvtest2 (cost=0.00..22.30 rows=1230 width=36)
+ -> Hash (cost=28.45..28.45 rows=6 width=36)
+ -> Seq Scan on lvtest1 (cost=0.00..28.45 rows=6 width=36)
+ Filter: ((a % 2) = 0)
+(7 rows)
+
DROP SCHEMA temp_view_test CASCADE;
NOTICE: drop cascades to 22 other objects
DETAIL: drop cascades to table temp_view_test.base_table
@@ -264,7 +316,7 @@ drop cascades to view temp_view_test.v8
drop cascades to sequence temp_view_test.seq1
drop cascades to view temp_view_test.v9
DROP SCHEMA testviewschm2 CASCADE;
-NOTICE: drop cascades to 16 other objects
+NOTICE: drop cascades to 20 other objects
DETAIL: drop cascades to table t1
drop cascades to view temporal1
drop cascades to view temporal2
@@ -281,4 +333,8 @@ drop cascades to table tbl3
drop cascades to table tbl4
drop cascades to view mytempview
drop cascades to view pubview
+drop cascades to table lvtest1
+drop cascades to table lvtest2
+drop cascades to function f_leak(text)
+drop cascades to view leaky_view
SET search_path to public;
diff --git a/src/test/regress/sql/create_view.sql b/src/test/regress/sql/create_view.sql
index 86cfc51..d9b7fa1 100644
--- a/src/test/regress/sql/create_view.sql
+++ b/src/test/regress/sql/create_view.sql
@@ -191,6 +191,27 @@ AND NOT EXISTS (SELECT g FROM tbl4 LEFT JOIN tmptbl ON tbl4.h = tmptbl.j);
SELECT count(*) FROM pg_class where relname LIKE 'mytempview'
And relnamespace IN (SELECT OID FROM pg_namespace WHERE nspname LIKE 'pg_temp%');
+--Should work correctly to leaky-view scenario
+CREATE TABLE lvtest1 (a int, b text);
+CREATE TABLE lvtest2 (x int, y text);
+CREATE FUNCTION f_leak(text) RETURNS bool LANGUAGE 'plpgsql'
+AS 'BEGIN raise notice ''leak => %'', $1; RETURN true; END';
+
+INSERT INTO lvtest1 VALUES (10, 'aaa'), (11, 'bbb'), (12, 'ccc'), (13, 'ddd');
+INSERT INTO lvtest2 VALUES (11, 'xxx'), (12, 'yyy'), (13, 'zzz'), (14, 'xyz');
+
+CREATE OR REPLACE VIEW leaky_view AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 0;
+
+SELECT * FROM leaky_view WHERE f_leak(y);
+EXPLAIN SELECT * FROM leaky_view WHERE f_leak(y);
+
+CREATE OR REPLACE SECURITY VIEW leaky_view AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 0;
+
+SELECT * FROM leaky_view WHERE f_leak(y);
+EXPLAIN SELECT * FROM leaky_view WHERE f_leak(y);
+
DROP SCHEMA temp_view_test CASCADE;
DROP SCHEMA testviewschm2 CASCADE;