doc/src/sgml/rules.sgml | 66 ++++++++++++ src/backend/nodes/copyfuncs.c | 1 + src/backend/nodes/equalfuncs.c | 1 + src/backend/nodes/outfuncs.c | 1 + src/backend/nodes/readfuncs.c | 1 + src/backend/optimizer/plan/initsplan.c | 96 ++++++++++++++---- src/backend/optimizer/prep/prepjointree.c | 7 ++ src/backend/optimizer/util/clauses.c | 113 ++++++++++++++++++++ src/backend/utils/cache/lsyscache.c | 19 ++++ src/include/nodes/primnodes.h | 1 + src/include/optimizer/clauses.h | 1 + src/include/utils/lsyscache.h | 1 + src/test/regress/expected/select_views.out | 153 +++++++++++++++++++++++++--- src/test/regress/sql/select_views.sql | 67 ++++++++++++ 14 files changed, 493 insertions(+), 35 deletions(-) diff --git a/doc/src/sgml/rules.sgml b/doc/src/sgml/rules.sgml index 1b06519..1b5ae6f 100644 --- a/doc/src/sgml/rules.sgml +++ b/doc/src/sgml/rules.sgml @@ -1856,6 +1856,72 @@ SELECT * FROM phone_number WHERE tricky(person, phone); + In addition, you might be able to leak contents of invisible tuples + using the following scenario: + +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(); + + This view also might seem secure, since any SELECT + from your_credit shall be rewritten into a + SELECT from the join of pg_authid + and 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. + +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) + + The reason is obvious from the result of EXPLAIN. + +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) + + The supplied tricky only references + number and expire columns, + however, the qualifier to filter invisible tuples performs on + the scan of pg_authid. + Then, since the optimizer tries to minimize the number of tuples + being joined, the supplied qualifer got attached on the scan of + credit_cards. + In the result, it allows tricky to reference + contents of the credit_cards table. + + + The security_berrier option of views enables + to prevent both of the scenarios, instead of a bit performance + trade-off. If and when a particular view is defined with + security_berrier=TRUE, any exogenetic qualifiers + cannot be pushed-down except for a limited number of trusted + operators being transformed into index scan, even if the supplied + qualifier references only one-side of relation joins. In addition, + underlying qualifiers of security barrier view shall be launched + earlier than others, even if sub-queries are pulled-up and + qualifiers got merged due to the optimization. + + + 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 SELECT, diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 4fb60a1..2af5113 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_barrier); return newnode; } diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index d201f22..4e001f7 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_barrier); return true; } diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 04a8760..3a263aa 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_barrier); } /***************************************************************************** diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index 098f3c3..b7fcb25 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -1161,6 +1161,7 @@ _readFromExpr(void) READ_NODE_FIELD(fromlist); READ_NODE_FIELD(quals); + READ_BOOL_FIELD(security_barrier); READ_DONE(); } diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c index 333ede2..cd8c499 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_barrier, &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_barrier, + &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 ac622a3..ae96b4f 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -705,6 +705,13 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, pull_up_subqueries(subroot, (Node *) subquery->jointree, NULL, NULL); /* + * If and when the sub-query was originally defined as a view with + * "security_barrier" option, we need to mark this FromExpr as a + * security barrier to prevent unexpected distribution of qualifiers. + */ + ((FromExpr *)subquery->jointree)->security_barrier = rte->security_barrier; + + /* * 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 be0935d..ddd1b0e 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 restrains 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)) + { + /* + * Right now, we have no way to distinguish safe functions with + * leakable ones, so, we treat all the function call possibly + * leakable. + */ + return true; + } + else if (IsA(node, OpExpr)) + { + OpExpr *expr = (OpExpr *) node; + + /* + * Right now, we assume operators implemented by built-in functions + * are not leakable, so it does not 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/utils/cache/lsyscache.c b/src/backend/utils/cache/lsyscache.c index 28d18b0..55571f1 100644 --- a/src/backend/utils/cache/lsyscache.c +++ b/src/backend/utils/cache/lsyscache.c @@ -1387,6 +1387,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/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index f1e20ef..cfef93f 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_barrier; /* Come from security-barrier view? */ } FromExpr; #endif /* PRIMNODES_H */ diff --git a/src/include/optimizer/clauses.h b/src/include/optimizer/clauses.h index dde6d82..09cf54f 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/test/regress/expected/select_views.out b/src/test/regress/expected/select_views.out index 6cd317c..5375aec 100644 --- a/src/test/regress/expected/select_views.out +++ b/src/test/regress/expected/select_views.out @@ -467,6 +467,20 @@ SELECT name, #thepath FROM iexit ORDER BY 1, 2; I- 580 | 21 I- 580 | 22 I- 580 | 22 + I- 580/I-680 Ramp | 2 + I- 580/I-680 Ramp | 2 + I- 580/I-680 Ramp | 2 + I- 580/I-680 Ramp | 2 + I- 580/I-680 Ramp | 2 + I- 580/I-680 Ramp | 2 + I- 580/I-680 Ramp | 4 + I- 580/I-680 Ramp | 4 + I- 580/I-680 Ramp | 4 + I- 580/I-680 Ramp | 4 + I- 580/I-680 Ramp | 5 + I- 580/I-680 Ramp | 6 + I- 580/I-680 Ramp | 6 + I- 580/I-680 Ramp | 6 I- 580 Ramp | 2 I- 580 Ramp | 2 I- 580 Ramp | 2 @@ -717,20 +731,6 @@ SELECT name, #thepath FROM iexit ORDER BY 1, 2; I- 580 Ramp | 8 I- 580 Ramp | 8 I- 580 Ramp | 8 - I- 580/I-680 Ramp | 2 - I- 580/I-680 Ramp | 2 - I- 580/I-680 Ramp | 2 - I- 580/I-680 Ramp | 2 - I- 580/I-680 Ramp | 2 - I- 580/I-680 Ramp | 2 - I- 580/I-680 Ramp | 4 - I- 580/I-680 Ramp | 4 - I- 580/I-680 Ramp | 4 - I- 580/I-680 Ramp | 4 - I- 580/I-680 Ramp | 5 - I- 580/I-680 Ramp | 6 - I- 580/I-680 Ramp | 6 - I- 580/I-680 Ramp | 6 I- 680 | 2 I- 680 | 2 I- 680 | 2 @@ -1247,3 +1247,128 @@ SELECT * FROM toyemp WHERE name = 'sharon'; sharon | 25 | (15,12) | 12000 (1 row) +-- +-- Test for leaky-vew +-- +CREATE USER alice; +CREATE FUNCTION f_leak(text, text) + RETURNS bool LANGUAGE 'plpgsql' + COST 0.00000001 + AS 'begin raise notice ''% => %'', $1, $2; return true; end'; +CREATE TABLE employee ( + eid int primary key, + ename text, + etitle text +); +NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "employee_pkey" for table "employee" +CREATE TABLE salary ( + eid int references employee(eid), + salary int, + ymd date +); +CREATE INDEX salary_ymd on salary (ymd); +INSERT INTO employee (eid, ename, etitle) + VALUES (100, 'alice', 'staff'), + (110, 'bob', 'manager'), + (120, 'eve', 'chief'); +INSERT INTO salary (eid, salary, ymd) + VALUES (100, 2000, '2011-06-01'), + (100, 2025, '2011-07-01'), + (110, 2500, '2011-06-01'), + (110, 2400, '2011-07-01'), + (120, 2200, '2011-07-01'); +CREATE VIEW my_salary_normal AS + SELECT ename,etitle,salary,ymd + FROM employee e JOIN salary s ON s.eid = s.eid + WHERE ename = getpgusername(); +CREATE VIEW my_salary_secure WITH (security_barrier) AS + SELECT ename,etitle,salary,ymd + FROM employee e JOIN salary s ON e.eid = s.eid + WHERE ename = getpgusername(); +GRANT SELECT ON my_salary_normal TO public; +GRANT SELECT ON my_salary_secure TO public; +-- run leaky view +SET SESSION AUTHORIZATION alice; +SELECT * FROM my_salary_normal + WHERE f_leak(ymd::text,salary::text); +NOTICE: 06-01-2011 => 2000 +NOTICE: 07-01-2011 => 2025 +NOTICE: 06-01-2011 => 2500 +NOTICE: 07-01-2011 => 2400 +NOTICE: 07-01-2011 => 2200 + ename | etitle | salary | ymd +-------+--------+--------+------------ + alice | staff | 2000 | 06-01-2011 + alice | staff | 2025 | 07-01-2011 + alice | staff | 2500 | 06-01-2011 + alice | staff | 2400 | 07-01-2011 + alice | staff | 2200 | 07-01-2011 +(5 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM my_salary_normal + WHERE f_leak(ymd::text,salary::text); + QUERY PLAN +----------------------------------------------------------------------------- + Nested Loop + -> Seq Scan on employee e + Filter: (ename = (getpgusername())::text) + -> Materialize + -> Seq Scan on salary s + Filter: ((eid = eid) AND f_leak((ymd)::text, (salary)::text)) +(6 rows) + +SELECT * FROM my_salary_secure + WHERE f_leak(ymd::text,salary::text); +NOTICE: 06-01-2011 => 2000 +NOTICE: 07-01-2011 => 2025 + ename | etitle | salary | ymd +-------+--------+--------+------------ + alice | staff | 2000 | 06-01-2011 + alice | staff | 2025 | 07-01-2011 +(2 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM my_salary_secure + WHERE f_leak(ymd::text,salary::text); + QUERY PLAN +-------------------------------------------------------------------------------------------------- + Hash Join + Hash Cond: (s.eid = e.eid) + Join Filter: ((e.ename = (getpgusername())::text) AND f_leak((s.ymd)::text, (s.salary)::text)) + -> Seq Scan on salary s + -> Hash + -> Seq Scan on employee e +(6 rows) + +SELECT * FROM my_salary_secure + WHERE f_leak(ymd::text,salary::text) AND ymd = '2011-06-01'; +NOTICE: 06-01-2011 => 2000 + ename | etitle | salary | ymd +-------+--------+--------+------------ + alice | staff | 2000 | 06-01-2011 +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM my_salary_secure + WHERE f_leak(ymd::text,salary::text) AND ymd = '2011-06-01'; + QUERY PLAN +-------------------------------------------------------------------------------------------------- + Hash Join + Hash Cond: (e.eid = s.eid) + Join Filter: ((e.ename = (getpgusername())::text) AND f_leak((s.ymd)::text, (s.salary)::text)) + -> Seq Scan on employee e + -> Hash + -> Bitmap Heap Scan on salary s + Recheck Cond: (ymd = '06-01-2011'::date) + -> Bitmap Index Scan on salary_ymd + Index Cond: (ymd = '06-01-2011'::date) +(9 rows) + +\c - +-- cleanups +DROP ROLE IF EXISTS alice; +DROP FUNCTION IF EXISTS f_leak(text); +NOTICE: function f_leak(text) does not exist, skipping +DROP TABLE IF EXISTS employee CASCADE; +NOTICE: drop cascades to 3 other objects +DETAIL: drop cascades to view my_salary_normal +drop cascades to constraint salary_eid_fkey on table salary +drop cascades to view my_salary_secure diff --git a/src/test/regress/sql/select_views.sql b/src/test/regress/sql/select_views.sql index 14f1be8..61151bc 100644 --- a/src/test/regress/sql/select_views.sql +++ b/src/test/regress/sql/select_views.sql @@ -8,3 +8,70 @@ SELECT * FROM street; SELECT name, #thepath FROM iexit ORDER BY 1, 2; SELECT * FROM toyemp WHERE name = 'sharon'; + +-- +-- Test for leaky-vew +-- + +CREATE USER alice; +CREATE FUNCTION f_leak(text, text) + RETURNS bool LANGUAGE 'plpgsql' + COST 0.00000001 + AS 'begin raise notice ''% => %'', $1, $2; return true; end'; + +CREATE TABLE employee ( + eid int primary key, + ename text, + etitle text +); + +CREATE TABLE salary ( + eid int references employee(eid), + salary int, + ymd date +); +CREATE INDEX salary_ymd on salary (ymd); + +INSERT INTO employee (eid, ename, etitle) + VALUES (100, 'alice', 'staff'), + (110, 'bob', 'manager'), + (120, 'eve', 'chief'); +INSERT INTO salary (eid, salary, ymd) + VALUES (100, 2000, '2011-06-01'), + (100, 2025, '2011-07-01'), + (110, 2500, '2011-06-01'), + (110, 2400, '2011-07-01'), + (120, 2200, '2011-07-01'); +CREATE VIEW my_salary_normal AS + SELECT ename,etitle,salary,ymd + FROM employee e JOIN salary s ON s.eid = s.eid + WHERE ename = getpgusername(); +CREATE VIEW my_salary_secure WITH (security_barrier) AS + SELECT ename,etitle,salary,ymd + FROM employee e JOIN salary s ON e.eid = s.eid + WHERE ename = getpgusername(); + +GRANT SELECT ON my_salary_normal TO public; +GRANT SELECT ON my_salary_secure TO public; +-- run leaky view +SET SESSION AUTHORIZATION alice; + +SELECT * FROM my_salary_normal + WHERE f_leak(ymd::text,salary::text); +EXPLAIN (COSTS OFF) SELECT * FROM my_salary_normal + WHERE f_leak(ymd::text,salary::text); + +SELECT * FROM my_salary_secure + WHERE f_leak(ymd::text,salary::text); +EXPLAIN (COSTS OFF) SELECT * FROM my_salary_secure + WHERE f_leak(ymd::text,salary::text); + +SELECT * FROM my_salary_secure + WHERE f_leak(ymd::text,salary::text) AND ymd = '2011-06-01'; +EXPLAIN (COSTS OFF) SELECT * FROM my_salary_secure + WHERE f_leak(ymd::text,salary::text) AND ymd = '2011-06-01'; +\c - +-- cleanups +DROP ROLE IF EXISTS alice; +DROP FUNCTION IF EXISTS f_leak(text); +DROP TABLE IF EXISTS employee CASCADE; \ No newline at end of file