Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit a819012

Browse files
author
Commitfest Bot
committed
[CF 5018] v4 - Extension security improvement: Add support for extensions with an owned schema
This branch was automatically generated by a robot using patches from an email thread registered at: https://commitfest.postgresql.org/patch/5018 The branch will be overwritten each time a new patch version is posted to the thread, and also periodically to check for bitrot caused by changes on the master branch. Patch(es): https://www.postgresql.org/message-id/CAGECzQS02M6YPDXemo36tShO-ZYObjqnyTJyVttua1PGyN4xRw@mail.gmail.com Author(s): Jelte Fennema-Nio
2 parents e29df42 + 0d5e3e3 commit a819012

17 files changed

+326
-62
lines changed

doc/src/sgml/extend.sgml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,40 @@ RETURNS anycompatible AS ...
814814
</listitem>
815815
</varlistentry>
816816

817+
<varlistentry id="extend-extensions-files-owned-schema">
818+
<term><varname>owned_schema</varname> (<type>boolean</type>)</term>
819+
<listitem>
820+
<para>
821+
An extension should set <firstterm>owned_schema</firstterm> to
822+
<literal>true</literal> in its control file if the extension wants a
823+
dedicated schema for its objects. Such a schema should not exist yet at
824+
the time of extension creation, and will be created automatically by
825+
<literal>CREATE EXTENSION</literal>. The default is
826+
<literal>false</literal>, i.e., the extension can be installed into an
827+
existing schema.
828+
</para>
829+
<para>
830+
Having a schema owned by the extension can make it much easier to
831+
reason about possible <literal>search_path</literal> injection attacks.
832+
For instance with an owned schema, it is generally safe to set the
833+
<literal>search_path</literal> of a <literal>SECURITY DEFINER</literal>
834+
function to the schema of the extension. While without an owned schema
835+
it might not be safe to do so, because a malicious user could insert
836+
objects in that schema and thus <link
837+
linkend="sql-createfunction-security"> cause malicious to be executed
838+
as superuser</link>. Similarly, having an owned schema can make it safe
839+
by default to execute general-purpose SQL in the extension script,
840+
because the search_path now only contains trusted schemas. Without an
841+
owned schema it's <link linkend="extend-extensions-security-scripts">
842+
recommended to manually change the search_path</link>.
843+
</para>
844+
<para>
845+
Apart from the security considerations, having an owned schema can help
846+
prevent naming conflicts between objects of different extensions.
847+
</para>
848+
</listitem>
849+
</varlistentry>
850+
817851
<varlistentry id="extend-extensions-files-schema">
818852
<term><varname>schema</varname> (<type>string</type>)</term>
819853
<listitem>

doc/src/sgml/ref/create_extension.sgml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ CREATE EXTENSION [ IF NOT EXISTS ] <replaceable class="parameter">extension_name
104104
<para>
105105
The name of the schema in which to install the extension's
106106
objects, given that the extension allows its contents to be
107-
relocated. The named schema must already exist.
107+
relocated. The named schema must already exist, unless
108+
<literal>owned_schema</literal> is set to <literal>true</literal> in
109+
the control file, then the schema must not exist.
108110
If not specified, and the extension's control file does not specify a
109111
schema either, the current default object creation schema is used.
110112
</para>

src/backend/commands/extension.c

Lines changed: 104 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ typedef struct ExtensionControlFile
9090
* MODULE_PATHNAME */
9191
char *comment; /* comment, if any */
9292
char *schema; /* target schema (allowed if !relocatable) */
93+
bool owned_schema; /* if the schema should be owned by the
94+
* extension */
9395
bool relocatable; /* is ALTER EXTENSION SET SCHEMA supported? */
9496
bool superuser; /* must be superuser to install? */
9597
bool trusted; /* allow becoming superuser on the fly? */
@@ -611,6 +613,14 @@ parse_extension_control_file(ExtensionControlFile *control,
611613
{
612614
control->schema = pstrdup(item->value);
613615
}
616+
else if (strcmp(item->name, "owned_schema") == 0)
617+
{
618+
if (!parse_bool(item->value, &control->owned_schema))
619+
ereport(ERROR,
620+
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
621+
errmsg("parameter \"%s\" requires a Boolean value",
622+
item->name)));
623+
}
614624
else if (strcmp(item->name, "relocatable") == 0)
615625
{
616626
if (!parse_bool(item->value, &control->relocatable))
@@ -1744,8 +1754,11 @@ CreateExtensionInternal(char *extensionName,
17441754
*/
17451755
if (schemaName)
17461756
{
1747-
/* If the user is giving us the schema name, it must exist already. */
1748-
schemaOid = get_namespace_oid(schemaName, false);
1757+
/*
1758+
* If the user is giving us the schema name, it must exist already if
1759+
* the extension does not want to own the schema
1760+
*/
1761+
schemaOid = get_namespace_oid(schemaName, control->owned_schema);
17491762
}
17501763

17511764
if (control->schema != NULL)
@@ -1767,7 +1780,10 @@ CreateExtensionInternal(char *extensionName,
17671780

17681781
/* Always use the schema from control file for current extension. */
17691782
schemaName = control->schema;
1783+
}
17701784

1785+
if (schemaName)
1786+
{
17711787
/* Find or create the schema in case it does not exist. */
17721788
schemaOid = get_namespace_oid(schemaName, true);
17731789

@@ -1788,8 +1804,22 @@ CreateExtensionInternal(char *extensionName,
17881804
*/
17891805
schemaOid = get_namespace_oid(schemaName, false);
17901806
}
1807+
else if (control->owned_schema)
1808+
{
1809+
ereport(ERROR,
1810+
(errcode(ERRCODE_DUPLICATE_SCHEMA),
1811+
errmsg("schema \"%s\" already exists",
1812+
schemaName)));
1813+
}
1814+
17911815
}
1792-
else if (!OidIsValid(schemaOid))
1816+
else if (control->owned_schema)
1817+
{
1818+
ereport(ERROR,
1819+
(errcode(ERRCODE_UNDEFINED_SCHEMA),
1820+
errmsg("no schema has been selected to create in")));
1821+
}
1822+
else
17931823
{
17941824
/*
17951825
* Neither user nor author of the extension specified schema; use the
@@ -1856,6 +1886,7 @@ CreateExtensionInternal(char *extensionName,
18561886
*/
18571887
address = InsertExtensionTuple(control->name, extowner,
18581888
schemaOid, control->relocatable,
1889+
control->owned_schema,
18591890
versionName,
18601891
PointerGetDatum(NULL),
18611892
PointerGetDatum(NULL),
@@ -2061,7 +2092,8 @@ CreateExtension(ParseState *pstate, CreateExtensionStmt *stmt)
20612092
*/
20622093
ObjectAddress
20632094
InsertExtensionTuple(const char *extName, Oid extOwner,
2064-
Oid schemaOid, bool relocatable, const char *extVersion,
2095+
Oid schemaOid, bool relocatable, bool ownedSchema,
2096+
const char *extVersion,
20652097
Datum extConfig, Datum extCondition,
20662098
List *requiredExtensions)
20672099
{
@@ -2091,6 +2123,7 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
20912123
values[Anum_pg_extension_extowner - 1] = ObjectIdGetDatum(extOwner);
20922124
values[Anum_pg_extension_extnamespace - 1] = ObjectIdGetDatum(schemaOid);
20932125
values[Anum_pg_extension_extrelocatable - 1] = BoolGetDatum(relocatable);
2126+
values[Anum_pg_extension_extownedschema - 1] = BoolGetDatum(ownedSchema);
20942127
values[Anum_pg_extension_extversion - 1] = CStringGetTextDatum(extVersion);
20952128

20962129
if (extConfig == PointerGetDatum(NULL))
@@ -2135,6 +2168,17 @@ InsertExtensionTuple(const char *extName, Oid extOwner,
21352168
record_object_address_dependencies(&myself, refobjs, DEPENDENCY_NORMAL);
21362169
free_object_addresses(refobjs);
21372170

2171+
if (ownedSchema)
2172+
{
2173+
ObjectAddress schemaAddress = {
2174+
.classId = NamespaceRelationId,
2175+
.objectId = schemaOid,
2176+
};
2177+
2178+
recordDependencyOn(&schemaAddress, &myself, DEPENDENCY_EXTENSION);
2179+
}
2180+
2181+
21382182
/* Post creation hook for new extension */
21392183
InvokeObjectPostCreateHook(ExtensionRelationId, extensionOid, 0);
21402184

@@ -3053,11 +3097,10 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
30533097
HeapTuple depTup;
30543098
ObjectAddresses *objsMoved;
30553099
ObjectAddress extAddr;
3100+
bool ownedSchema;
30563101

30573102
extensionOid = get_extension_oid(extensionName, false);
30583103

3059-
nspOid = LookupCreationNamespace(newschema);
3060-
30613104
/*
30623105
* Permission check: must own extension. Note that we don't bother to
30633106
* check ownership of the individual member objects ...
@@ -3066,22 +3109,6 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
30663109
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_EXTENSION,
30673110
extensionName);
30683111

3069-
/* Permission check: must have creation rights in target namespace */
3070-
aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
3071-
if (aclresult != ACLCHECK_OK)
3072-
aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
3073-
3074-
/*
3075-
* If the schema is currently a member of the extension, disallow moving
3076-
* the extension into the schema. That would create a dependency loop.
3077-
*/
3078-
if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
3079-
ereport(ERROR,
3080-
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
3081-
errmsg("cannot move extension \"%s\" into schema \"%s\" "
3082-
"because the extension contains the schema",
3083-
extensionName, newschema)));
3084-
30853112
/* Locate the pg_extension tuple */
30863113
extRel = table_open(ExtensionRelationId, RowExclusiveLock);
30873114

@@ -3105,14 +3132,38 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
31053132

31063133
systable_endscan(extScan);
31073134

3108-
/*
3109-
* If the extension is already in the target schema, just silently do
3110-
* nothing.
3111-
*/
3112-
if (extForm->extnamespace == nspOid)
3135+
ownedSchema = extForm->extownedschema;
3136+
3137+
if (!ownedSchema)
31133138
{
3114-
table_close(extRel, RowExclusiveLock);
3115-
return InvalidObjectAddress;
3139+
nspOid = LookupCreationNamespace(newschema);
3140+
3141+
/* Permission check: must have creation rights in target namespace */
3142+
aclresult = object_aclcheck(NamespaceRelationId, nspOid, GetUserId(), ACL_CREATE);
3143+
if (aclresult != ACLCHECK_OK)
3144+
aclcheck_error(aclresult, OBJECT_SCHEMA, newschema);
3145+
3146+
/*
3147+
* If the schema is currently a member of the extension, disallow
3148+
* moving the extension into the schema. That would create a
3149+
* dependency loop.
3150+
*/
3151+
if (getExtensionOfObject(NamespaceRelationId, nspOid) == extensionOid)
3152+
ereport(ERROR,
3153+
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
3154+
errmsg("cannot move extension \"%s\" into schema \"%s\" "
3155+
"because the extension contains the schema",
3156+
extensionName, newschema)));
3157+
3158+
/*
3159+
* If the extension is already in the target schema, just silently do
3160+
* nothing.
3161+
*/
3162+
if (extForm->extnamespace == nspOid)
3163+
{
3164+
table_close(extRel, RowExclusiveLock);
3165+
return InvalidObjectAddress;
3166+
}
31163167
}
31173168

31183169
/* Check extension is supposed to be relocatable */
@@ -3185,6 +3236,13 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
31853236
}
31863237
}
31873238

3239+
/*
3240+
* We don't actually have to move any objects anything for owned
3241+
* schemas, because we simply rename the schema.
3242+
*/
3243+
if (ownedSchema)
3244+
continue;
3245+
31883246
/*
31893247
* Otherwise, ignore non-membership dependencies. (Currently, the
31903248
* only other case we could see here is a normal dependency from
@@ -3228,18 +3286,26 @@ AlterExtensionNamespace(const char *extensionName, const char *newschema, Oid *o
32283286

32293287
relation_close(depRel, AccessShareLock);
32303288

3231-
/* Now adjust pg_extension.extnamespace */
3232-
extForm->extnamespace = nspOid;
3289+
if (ownedSchema)
3290+
{
3291+
RenameSchema(get_namespace_name(oldNspOid), newschema);
3292+
table_close(extRel, RowExclusiveLock);
3293+
}
3294+
else
3295+
{
3296+
/* Now adjust pg_extension.extnamespace */
3297+
extForm->extnamespace = nspOid;
32333298

3234-
CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
3299+
CatalogTupleUpdate(extRel, &extTup->t_self, extTup);
32353300

3236-
table_close(extRel, RowExclusiveLock);
3301+
table_close(extRel, RowExclusiveLock);
32373302

3238-
/* update dependency to point to the new schema */
3239-
if (changeDependencyFor(ExtensionRelationId, extensionOid,
3240-
NamespaceRelationId, oldNspOid, nspOid) != 1)
3241-
elog(ERROR, "could not change schema dependency for extension %s",
3242-
NameStr(extForm->extname));
3303+
/* update dependency to point to the new schema */
3304+
if (changeDependencyFor(ExtensionRelationId, extensionOid,
3305+
NamespaceRelationId, oldNspOid, nspOid) != 1)
3306+
elog(ERROR, "could not change schema dependency for extension %s",
3307+
NameStr(extForm->extname));
3308+
}
32433309

32443310
InvokeObjectPostAlterHook(ExtensionRelationId, extensionOid, 0);
32453311

src/backend/utils/adt/pg_upgrade_support.c

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "catalog/pg_subscription_rel.h"
2020
#include "catalog/pg_type.h"
2121
#include "commands/extension.h"
22+
#include "commands/schemacmds.h"
2223
#include "miscadmin.h"
2324
#include "replication/logical.h"
2425
#include "replication/origin.h"
@@ -184,41 +185,45 @@ Datum
184185
binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
185186
{
186187
text *extName;
187-
text *schemaName;
188+
char *schemaName;
188189
bool relocatable;
190+
bool ownedschema;
189191
text *extVersion;
190192
Datum extConfig;
191193
Datum extCondition;
192194
List *requiredExtensions;
195+
Oid schemaOid;
193196

194197
CHECK_IS_BINARY_UPGRADE;
195198

196199
/* We must check these things before dereferencing the arguments */
197200
if (PG_ARGISNULL(0) ||
198201
PG_ARGISNULL(1) ||
199202
PG_ARGISNULL(2) ||
200-
PG_ARGISNULL(3))
203+
PG_ARGISNULL(3) ||
204+
PG_ARGISNULL(4))
201205
elog(ERROR, "null argument to binary_upgrade_create_empty_extension is not allowed");
202206

203207
extName = PG_GETARG_TEXT_PP(0);
204-
schemaName = PG_GETARG_TEXT_PP(1);
208+
schemaName = text_to_cstring(PG_GETARG_TEXT_PP(1));
205209
relocatable = PG_GETARG_BOOL(2);
206-
extVersion = PG_GETARG_TEXT_PP(3);
210+
ownedschema = PG_GETARG_BOOL(3);
211+
extVersion = PG_GETARG_TEXT_PP(4);
207212

208-
if (PG_ARGISNULL(4))
213+
if (PG_ARGISNULL(5))
209214
extConfig = PointerGetDatum(NULL);
210215
else
211-
extConfig = PG_GETARG_DATUM(4);
216+
extConfig = PG_GETARG_DATUM(5);
212217

213-
if (PG_ARGISNULL(5))
218+
if (PG_ARGISNULL(6))
214219
extCondition = PointerGetDatum(NULL);
215220
else
216-
extCondition = PG_GETARG_DATUM(5);
221+
extCondition = PG_GETARG_DATUM(6);
217222

218223
requiredExtensions = NIL;
219-
if (!PG_ARGISNULL(6))
224+
if (!PG_ARGISNULL(7))
220225
{
221-
ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(6);
226+
ArrayType *textArray = PG_GETARG_ARRAYTYPE_P(7);
222227
Datum *textDatums;
223228
int ndatums;
224229
int i;
@@ -233,10 +238,28 @@ binary_upgrade_create_empty_extension(PG_FUNCTION_ARGS)
233238
}
234239
}
235240

241+
if (ownedschema)
242+
{
243+
CreateSchemaStmt *csstmt = makeNode(CreateSchemaStmt);
244+
245+
csstmt->schemaname = schemaName;
246+
csstmt->authrole = NULL; /* will be created by current user */
247+
csstmt->schemaElts = NIL;
248+
csstmt->if_not_exists = false;
249+
schemaOid = CreateSchemaCommand(csstmt, "(generated CREATE SCHEMA command)",
250+
-1, -1);
251+
252+
}
253+
else
254+
{
255+
schemaOid = get_namespace_oid(schemaName, false);
256+
}
257+
236258
InsertExtensionTuple(text_to_cstring(extName),
237259
GetUserId(),
238-
get_namespace_oid(text_to_cstring(schemaName), false),
260+
schemaOid,
239261
relocatable,
262+
ownedschema,
240263
text_to_cstring(extVersion),
241264
extConfig,
242265
extCondition,

0 commit comments

Comments
 (0)