Skip to content

Commit 449618b

Browse files
authored
Update security restrictions to allow non-superuser extension installation (#572)
This updates a bunch of our security related code. For previous releases we needed to be very careful with allowing arbitrary SQL code to be executed in DuckDB because DuckDB queries could read all Postgres tables. This is not the case anymore since #466 was merged, because now any access to Postgres tables goes through the Postgres planner and executor instead of custom code. Lots of code wasn't updated with that new behaviour in mind though. 1. Allow running `duckdb.raw_query`, `duckdb.cache`, `duckdb.cache_info`, `duckdb.cache_delete` and `duckdb.recycle_db` as any user (with the duckdb role). 2. Allow running `duckdb.install_extension` as regular users, if required permissions are explicitly granted. This is not allowed by default for non-superusers because it's still considered a very high privilege. 3. Disallow running queries on tables with RLS enabled in a different place, so that it is checked for every Postgres table that DuckDB opens (also when using `duckdb.query`/`duckdb.raw_query`). 4. Add `duckdb.allow_community_extensions` setting.
1 parent d9bb1a3 commit 449618b

14 files changed

+231
-48
lines changed

include/pgduckdb/pgduckdb_guc.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ extern int duckdb_maximum_threads;
55
extern char *duckdb_maximum_memory;
66
extern char *duckdb_disabled_filesystems;
77
extern bool duckdb_enable_external_access;
8+
extern bool duckdb_allow_community_extensions;
89
extern bool duckdb_allow_unsigned_extensions;
910
extern bool duckdb_autoinstall_known_extensions;
1011
extern bool duckdb_autoload_known_extensions;

include/pgduckdb/pgduckdb_metadata_cache.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ Oid IsDuckdbTable(Relation relation);
2121
Oid IsMotherDuckTable(Form_pg_class relation);
2222
Oid IsMotherDuckTable(Relation relation);
2323
Oid IsDuckdbExecutionAllowed();
24+
void RequireDuckdbExecution();
2425
} // namespace pgduckdb

sql/pg_duckdb--0.2.0--0.3.0.sql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,3 +1050,9 @@ RETURNS duckdb.unresolved_type
10501050
SET search_path = pg_catalog, pg_temp
10511051
AS 'MODULE_PATHNAME', 'duckdb_only_function'
10521052
LANGUAGE C;
1053+
1054+
GRANT ALL ON FUNCTION duckdb.raw_query(TEXT) TO PUBLIC;
1055+
GRANT ALL ON FUNCTION duckdb.cache(TEXT, TEXT) TO PUBLIC;
1056+
GRANT ALL ON FUNCTION duckdb.cache_info() TO PUBLIC;
1057+
GRANT ALL ON FUNCTION duckdb.cache_delete(TEXT) TO PUBLIC;
1058+
GRANT ALL ON PROCEDURE duckdb.recycle_ddb() TO PUBLIC;

src/pg/relations.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ extern "C" {
1111
#include "utils/builtins.h"
1212
#include "utils/lsyscache.h"
1313
#include "utils/rel.h"
14+
#include "utils/rls.h"
1415
#include "utils/resowner.h" // CurrentResourceOwner and TopTransactionResourceOwner
1516
#include "executor/tuptable.h" // TupIsNull
1617
#include "utils/syscache.h" // RELOID
@@ -70,6 +71,12 @@ SlotGetAllAttrs(TupleTableSlot *slot) {
7071

7172
Relation
7273
OpenRelation(Oid relationId) {
74+
if (PostgresFunctionGuard(check_enable_rls, relationId, InvalidOid, false) == RLS_ENABLED) {
75+
throw duckdb::NotImplementedException(
76+
"Cannot use \"%s\" relation in a DuckDB query, because RLS is enabled on it",
77+
PostgresFunctionGuard(get_rel_name, relationId));
78+
}
79+
7380
/*
7481
* We always open & close the relation using the
7582
* TopTransactionResourceOwner to avoid having to close the relation

src/pgduckdb.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ int duckdb_maximum_threads = -1;
2626
char *duckdb_maximum_memory = strdup("4GB");
2727
char *duckdb_disabled_filesystems = strdup("LocalFileSystem");
2828
bool duckdb_enable_external_access = true;
29+
bool duckdb_allow_community_extensions = false;
2930
bool duckdb_allow_unsigned_extensions = false;
3031
bool duckdb_autoinstall_known_extensions = true;
3132
bool duckdb_autoload_known_extensions = true;
@@ -130,6 +131,9 @@ DuckdbInitGUC(void) {
130131
DefineCustomVariable("duckdb.enable_external_access", "Allow the DuckDB to access external state.",
131132
&duckdb_enable_external_access, PGC_SUSET);
132133

134+
DefineCustomVariable("duckdb.allow_community_extensions", "Disable installing community extensions",
135+
&duckdb_allow_community_extensions, PGC_SUSET);
136+
133137
DefineCustomVariable("duckdb.allow_unsigned_extensions",
134138
"Allow DuckDB to load extensions with invalid or missing signatures",
135139
&duckdb_allow_unsigned_extensions, PGC_SUSET);

src/pgduckdb_duckdb.cpp

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ DuckDBManager::Initialize() {
137137

138138
SET_DUCKDB_OPTION(allow_unsigned_extensions);
139139
SET_DUCKDB_OPTION(enable_external_access);
140+
SET_DUCKDB_OPTION(allow_community_extensions);
140141
SET_DUCKDB_OPTION(autoinstall_known_extensions);
141142
SET_DUCKDB_OPTION(autoload_known_extensions);
142143

@@ -344,9 +345,7 @@ DuckDBManager::RefreshConnectionState(duckdb::ClientContext &context) {
344345
*/
345346
duckdb::unique_ptr<duckdb::Connection>
346347
DuckDBManager::CreateConnection() {
347-
if (!pgduckdb::IsDuckdbExecutionAllowed()) {
348-
elog(ERROR, "DuckDB execution is not allowed because you have not been granted the duckdb.postgres_role");
349-
}
348+
pgduckdb::RequireDuckdbExecution();
350349

351350
auto &instance = Get();
352351
auto connection = duckdb::make_uniq<duckdb::Connection>(*instance.database);
@@ -360,9 +359,7 @@ DuckDBManager::CreateConnection() {
360359
/* Returns the cached connection to the global DuckDB instance. */
361360
duckdb::Connection *
362361
DuckDBManager::GetConnection(bool force_transaction) {
363-
if (!pgduckdb::IsDuckdbExecutionAllowed()) {
364-
elog(ERROR, "DuckDB execution is not allowed because you have not been granted the duckdb.postgres_role");
365-
}
362+
pgduckdb::RequireDuckdbExecution();
366363

367364
auto &instance = Get();
368365
auto &context = *instance.connection->context;

src/pgduckdb_metadata_cache.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,4 +347,11 @@ IsDuckdbExecutionAllowed() {
347347
return has_privs_of_role(GetUserId(), cache.postgres_role_oid);
348348
}
349349

350+
void
351+
RequireDuckdbExecution() {
352+
if (!pgduckdb::IsDuckdbExecutionAllowed()) {
353+
elog(ERROR, "DuckDB execution is not allowed because you have not been granted the duckdb.postgres_role");
354+
}
355+
}
356+
350357
} // namespace pgduckdb

src/pgduckdb_options.cpp

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ extern "C" {
2424
#include "nodes/nodeFuncs.h"
2525
#include "catalog/namespace.h"
2626
#include "utils/builtins.h"
27+
#include "utils/guc.h"
2728
#include "utils/lsyscache.h"
2829
#include "utils/rel.h"
2930
#include "utils/snapmgr.h"
@@ -193,8 +194,28 @@ ReadDuckdbExtensions() {
193194
static bool
194195
DuckdbInstallExtension(Datum name_datum) {
195196
auto extension_name = DatumToString(name_datum);
196-
auto install_extension_command = duckdb::StringUtil::Format("INSTALL %s;", extension_name.c_str());
197+
198+
auto install_extension_command = "INSTALL " + duckdb::KeywordHelper::WriteQuoted(extension_name);
199+
200+
/*
201+
* Temporily allow all filesystems for this query, because INSTALL needs
202+
* local filesystem access. Since this setting cannot be changed inside
203+
* DuckDB after it's set to LocalFileSystem this temporary configuration
204+
* change only really has effect duckdb.install_extension is called as the
205+
* first DuckDB query for this session. Since we cannot change it back.
206+
*
207+
* While that's suboptimal it's also not a huge problem. Users only need to
208+
* install an extension once on a server. So doing that on a new connection
209+
* or after calling duckdb.recycle_ddb() should not be a big deal.
210+
*
211+
* NOTE: Because each backend has its own DuckDB instance, this setting
212+
* does not impact other backends and thus cannot cause a security issue
213+
* due to a race condition.
214+
*/
215+
auto save_nestlevel = NewGUCNestLevel();
216+
SetConfigOption("duckdb.disabled_filesystems", "", PGC_SUSET, PGC_S_SESSION);
197217
pgduckdb::DuckDBQueryOrThrow(install_extension_command);
218+
AtEOXact_GUC(false, save_nestlevel);
198219

199220
Oid arg_types[] = {TEXTOID};
200221
Datum values[] = {name_datum};
@@ -319,6 +340,7 @@ DECLARE_PG_FUNCTION(cache) {
319340
}
320341

321342
DECLARE_PG_FUNCTION(cache_info) {
343+
pgduckdb::RequireDuckdbExecution();
322344
ReturnSetInfo *rsinfo = (ReturnSetInfo *)fcinfo->resultinfo;
323345
Tuplestorestate *tuple_store;
324346
TupleDesc cache_info_tuple_desc;
@@ -359,12 +381,14 @@ DECLARE_PG_FUNCTION(cache_info) {
359381
}
360382

361383
DECLARE_PG_FUNCTION(cache_delete) {
384+
pgduckdb::RequireDuckdbExecution();
362385
Datum cache_key = PG_GETARG_DATUM(0);
363386
bool result = pgduckdb::DuckdbCacheDelete(cache_key);
364387
PG_RETURN_BOOL(result);
365388
}
366389

367390
DECLARE_PG_FUNCTION(pgduckdb_recycle_ddb) {
391+
pgduckdb::RequireDuckdbExecution();
368392
/*
369393
* We cannot safely run this in a transaction block, because a DuckDB
370394
* transaction might have already started. Recycling the database will

src/pgduckdb_planner.cpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,12 @@ DuckdbPlanNode(Query *parse, const char *query_string, int cursor_options, Param
164164
* actual plan with our CustomScan node. This is useful to get the correct
165165
* values for all the other many fields of the PLannedStmt.
166166
*
167-
* XXX: The primary reason we do this is that Postgres fills in permInfos
168-
* and rtable correctly. Those are needed for postgres to do its permission
169-
* checks on the used tables.
167+
* XXX: The primary reason we did this in the past is so that Postgres
168+
* filled in permInfos and rtable correctly. Those are needed for postgres
169+
* to do its permission checks on the used tables. We do these checks
170+
* inside DuckDB as well, so that's not really necessary anymore. We still
171+
* do this though to get all the other fields filled in correctly. Possibly
172+
* we don't need to do this anymore.
170173
*
171174
* FIXME: For some reason this needs an additional query copy to allow
172175
* re-planning of the query later during execution. But I don't really

src/pgduckdb_ruleutils.cpp

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -525,26 +525,6 @@ pgduckdb_relation_name(Oid relation_oid) {
525525
const char *postgres_schema_name = get_namespace_name_or_temp(relation->relnamespace);
526526
bool is_duckdb_table = pgduckdb::IsDuckdbTable(relation);
527527

528-
if (!is_duckdb_table) {
529-
/*
530-
* FIXME: This should be moved somewhere else. We already have a list
531-
* of RTEs somwhere that we use to call ExecCheckPermissions. We could
532-
* used that same list to check if RLS is enabled on any of the tables,
533-
* instead of checking it here for **every occurence** of each table in
534-
* the query. One benefit of having it here is that it ensures that we
535-
* never forget to check for RLS.
536-
*
537-
* NOTE: We only need to check this for non-DuckDB tables because DuckDB
538-
* tables don't support RLS anyway.
539-
*/
540-
if (check_enable_rls(relation_oid, InvalidOid, false) == RLS_ENABLED) {
541-
ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
542-
errmsg("(PGDuckDB/pgduckdb_relation_name) Cannot use \"%s\" in a DuckDB query, because RLS "
543-
"is enabled on it",
544-
get_rel_name(relation_oid))));
545-
}
546-
}
547-
548528
const char *db_and_schema = pgduckdb_db_and_schema_string(postgres_schema_name, is_duckdb_table);
549529

550530
char *result = psprintf("%s.%s", db_and_schema, quote_identifier(relname));

0 commit comments

Comments
 (0)