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

Commit 0d5e3e3

Browse files
JelteFCommitfest Bot
authored and
Commitfest Bot
committed
Add support for extensions with an owned schema
Writing the sql migration scripts that are run by CREATE EXTENSION and ALTER EXTENSION UPDATE are security minefields for extension authors. One big reason for this is that search_path is set to the schema of the extension while running these scripts, and thus if a user with lower privileges can create functions or operators in that schema they can do all kinds of search_path confusion attacks if not every function and operator that is used in the script is schema qualified. While doing such schema qualification is possible, it relies on the author to never make a mistake in any of the sql files. And sadly humans have a tendency to make mistakes. This patch adds a new "owned_schema" option to the extension control file that can be set to true to indicate that this extension wants to own the schema in which it is installed. What that means is that the schema should not exist before creating the extension, and will be created during extension creation. This thus gives the extension author an easy way to use a safe search_path, while still allowing all objects to be grouped together in a schema. The implementation also has the pleasant side effect that the schema will be automatically dropped when the extension is dropped.
1 parent e29df42 commit 0d5e3e3

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)