pgsql-v9.2-fix-leaky-view-part-0.v3.patch
application/octet-stream
Filename: pgsql-v9.2-fix-leaky-view-part-0.v3.patch
Type: application/octet-stream
Part: 0
doc/src/sgml/ref/create_view.sgml | 24 ++++++++++++++++++++
src/backend/access/common/reloptions.c | 15 ++++++++++++-
src/backend/commands/tablecmds.c | 3 +-
src/backend/commands/view.c | 29 +++++++++++++++++++-----
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/parser/gram.y | 10 +++++---
src/backend/rewrite/rewriteHandler.c | 1 +
src/backend/utils/cache/relcache.c | 1 +
src/bin/pg_dump/pg_dump.c | 6 +++-
src/include/access/reloptions.h | 3 +-
src/include/nodes/parsenodes.h | 3 ++
src/include/utils/rel.h | 11 ++++++++-
src/test/regress/expected/create_view.out | 34 ++++++++++++++++++++++++++++-
src/test/regress/sql/create_view.sql | 19 ++++++++++++++++
17 files changed, 146 insertions(+), 17 deletions(-)
diff --git a/doc/src/sgml/ref/create_view.sgml b/doc/src/sgml/ref/create_view.sgml
index 417f8c3..504ba27 100644
--- a/doc/src/sgml/ref/create_view.sgml
+++ b/doc/src/sgml/ref/create_view.sgml
@@ -22,6 +22,7 @@ PostgreSQL documentation
<refsynopsisdiv>
<synopsis>
CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] VIEW <replaceable class="PARAMETER">name</replaceable> [ ( <replaceable class="PARAMETER">column_name</replaceable> [, ...] ) ]
+ [ WITH ( parameter [= value] [, ... ] ) ]
AS <replaceable class="PARAMETER">query</replaceable>
</synopsis>
</refsynopsisdiv>
@@ -99,6 +100,29 @@ CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] VIEW <replaceable class="PARAMETER">n
</varlistentry>
<varlistentry>
+ <term><literal>WITH (<replaceable class="parameter">parameter [= value]</replaceable>)</literal></term>
+ <listitem>
+ <para>
+ This clause allows to specify optional parameters for a view.
+ </para>
+ <para>
+ If <literal>security_barrier=TRUE</literal> is specified, this view
+ shall performs as security barrier that prevent unexpected information
+ leaks. It is a recommendable configuration when the view is defined
+ to apply row-level security, 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">query</replaceable></term>
<listitem>
<para>
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 4657425..57f3b46 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_barrier",
+ "Prevent information leaks using functions with side-effects",
+ 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_barrier", RELOPT_TYPE_BOOL,
+ offsetof(StdRdOptions, security_barrier)},
};
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 cfc685b..8e67330 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -2926,7 +2926,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;
@@ -7881,6 +7881,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..92102e7 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, List *options)
{
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 ...
@@ -211,14 +214,11 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace)
*/
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,10 +229,24 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace)
atcmd->def = (Node *) lfirst(c);
atcmds = lappend(atcmds, atcmd);
}
- AlterTableInternal(viewOid, atcmds, true);
}
/*
+ * If optional parameters are specified, we must set options
+ * using ALTER TABLE SET OPTION internally.
+ */
+ if (list_length(options) > 0)
+ {
+ atcmd = makeNode(AlterTableCmd);
+ atcmd->subtype = AT_SetRelOptions;
+ atcmd->def = options;
+
+ atcmds = lappend(atcmds, atcmd);
+ }
+ if (atcmds != NIL)
+ AlterTableInternal(viewOid, atcmds, true);
+
+ /*
* Seems okay, so return the OID of the pre-existing view.
*/
relation_close(rel, NoLock); /* keep the lock! */
@@ -256,6 +270,9 @@ DefineVirtualRelation(const RangeVar *relation, List *tlist, bool replace)
createStmt->tablespacename = NULL;
createStmt->if_not_exists = false;
+ if (options != NIL)
+ createStmt->options = list_concat(createStmt->options, options);
+
/*
* 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 +527,7 @@ DefineView(ViewStmt *stmt, const char *queryString)
* aborted.
*/
viewOid = DefineVirtualRelation(view, viewParse->targetList,
- stmt->replace);
+ stmt->replace, stmt->options);
/*
* 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..4fb60a1 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -1944,6 +1944,7 @@ _copyRangeTblEntry(RangeTblEntry *from)
COPY_SCALAR_FIELD(relid);
COPY_SCALAR_FIELD(relkind);
COPY_NODE_FIELD(subquery);
+ COPY_SCALAR_FIELD(security_barrier);
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..d201f22 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2303,6 +2303,7 @@ _equalRangeTblEntry(RangeTblEntry *a, RangeTblEntry *b)
COMPARE_SCALAR_FIELD(relid);
COMPARE_SCALAR_FIELD(relkind);
COMPARE_NODE_FIELD(subquery);
+ COMPARE_SCALAR_FIELD(security_barrier);
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..04a8760 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2311,6 +2311,7 @@ _outRangeTblEntry(StringInfo str, RangeTblEntry *node)
break;
case RTE_SUBQUERY:
WRITE_NODE_FIELD(subquery);
+ WRITE_BOOL_FIELD(security_barrier);
break;
case RTE_JOIN:
WRITE_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 29a0e8f..098f3c3 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1191,6 +1191,7 @@ _readRangeTblEntry(void)
break;
case RTE_SUBQUERY:
READ_NODE_FIELD(subquery);
+ READ_BOOL_FIELD(security_barrier);
break;
case RTE_JOIN:
READ_ENUM_FIELD(jointype, JoinType);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7226032..517cdf3 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -7266,26 +7266,28 @@ transaction_mode_list_or_empty:
*
*****************************************************************************/
-ViewStmt: CREATE OptTemp VIEW qualified_name opt_column_list
+ViewStmt: CREATE OptTemp VIEW qualified_name opt_column_list opt_reloptions
AS SelectStmt opt_check_option
{
ViewStmt *n = makeNode(ViewStmt);
n->view = $4;
n->view->relpersistence = $2;
n->aliases = $5;
- n->query = $7;
+ n->query = $8;
n->replace = false;
+ n->options = $6;
$$ = (Node *) n;
}
- | CREATE OR REPLACE OptTemp VIEW qualified_name opt_column_list
+ | CREATE OR REPLACE OptTemp VIEW qualified_name opt_column_list opt_reloptions
AS SelectStmt opt_check_option
{
ViewStmt *n = makeNode(ViewStmt);
n->view = $6;
n->view->relpersistence = $4;
n->aliases = $7;
- n->query = $9;
+ n->query = $10;
n->replace = true;
+ n->options = $8;
$$ = (Node *) n;
}
;
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 6ef20a5..affe103 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -1382,6 +1382,7 @@ ApplyRetrieveRule(Query *parsetree,
rte->rtekind = RTE_SUBQUERY;
rte->relid = InvalidOid;
rte->subquery = rule_action;
+ rte->security_barrier = RelationIsSecurityView(relation);
rte->inh = false; /* must not be set for a subquery */
/*
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 0b9d77a..4eb1ee8 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/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 9e69b0f..ab2b0cb 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -12023,8 +12023,10 @@ dumpTableSchema(Archive *fout, TableInfo *tbinfo)
if (binary_upgrade)
binary_upgrade_set_pg_class_oids(q, tbinfo->dobj.catId.oid, false);
- appendPQExpBuffer(q, "CREATE VIEW %s AS\n %s\n",
- fmtId(tbinfo->dobj.name), viewdef);
+ appendPQExpBuffer(q, "CREATE VIEW %s", fmtId(tbinfo->dobj.name));
+ if (tbinfo->reloptions && strlen(tbinfo->reloptions) > 0)
+ appendPQExpBuffer(q, " WITH (%s)", tbinfo->reloptions);
+ appendPQExpBuffer(q, " AS\n %s\n", viewdef);
appendPQExpBuffer(labelq, "VIEW %s",
fmtId(tbinfo->dobj.name));
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 00c1269..4723c28 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_barrier; /* Was a security barrier view? */
+
/*
* Fields valid for a join RTE (else NULL/zero):
*
@@ -2339,6 +2341,7 @@ typedef struct ViewStmt
List *aliases; /* target column names */
Node *query; /* the SELECT query */
bool replace; /* replace an existing view? */
+ List *options; /* options from WITH clause */
} ViewStmt;
/* ----------------------
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index e2c2fa9..e1272af 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_barrier; /* performs as security-barrier view */
} 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_barrier : 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..7cc3000 100644
--- a/src/test/regress/expected/create_view.out
+++ b/src/test/regress/expected/create_view.out
@@ -239,6 +239,34 @@ 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);
+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_v1 WITH (security_barrier=true) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 0; -- OK
+CREATE OR REPLACE VIEW leaky_v2 WITH (security_barrier=false) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1; -- OK
+CREATE OR REPLACE VIEW leaky_v3 WITH (invalid_option=1234) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1; -- Fail
+ERROR: unrecognized parameter "invalid_option"
+SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+ relname | reloptions
+----------+--------------------------
+ leaky_v1 | {security_barrier=true}
+ leaky_v2 | {security_barrier=false}
+(2 rows)
+
+ALTER TABLE leaky_v1 RESET ( security_barrier );
+ALTER TABLE leaky_v2 SET ( security_barrier );
+SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+ relname | reloptions
+----------+-------------------------
+ leaky_v1 |
+ leaky_v2 | {security_barrier=true}
+(2 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 +292,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 +309,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 view leaky_v1
+drop cascades to view leaky_v2
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..fbcd25f 100644
--- a/src/test/regress/sql/create_view.sql
+++ b/src/test/regress/sql/create_view.sql
@@ -191,6 +191,25 @@ 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);
+
+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_v1 WITH (security_barrier=true) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 0; -- OK
+CREATE OR REPLACE VIEW leaky_v2 WITH (security_barrier=false) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1; -- OK
+CREATE OR REPLACE VIEW leaky_v3 WITH (invalid_option=1234) AS
+ SELECT * FROM lvtest1 JOIN lvtest2 ON a = x WHERE a % 2 = 1; -- Fail
+SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+
+ALTER TABLE leaky_v1 RESET ( security_barrier );
+ALTER TABLE leaky_v2 SET ( security_barrier );
+SELECT relname, reloptions FROM pg_class WHERE relname like 'leaky_v%';
+
DROP SCHEMA temp_view_test CASCADE;
DROP SCHEMA testviewschm2 CASCADE;