diff --git a/contrib/test_decoding/expected/permissions.out b/contrib/test_decoding/expected/permissions.out index ed97f81dda5..9b0670268ee 100644 --- a/contrib/test_decoding/expected/permissions.out +++ b/contrib/test_decoding/expected/permissions.out @@ -29,6 +29,17 @@ SELECT pg_drop_replication_slot('regression_slot'); (1 row) RESET ROLE; +-- no users can load an untrusted plugin +SET output_plugin_libraries = pgoutput; +SET ROLE regress_lr_replication; +SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding'); +ERROR: library "test_decoding" may not be used as an output plugin +HINT: If it is safe for all REPLICATION users to use this library as an output plugin, add it to "output_plugin_libraries" and reload the server configuration. +RESET ROLE; +SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding'); +ERROR: library "test_decoding" may not be used as an output plugin +HINT: If it is safe for all REPLICATION users to use this library as an output plugin, add it to "output_plugin_libraries" and reload the server configuration. +RESET output_plugin_libraries; -- replication user can control replication SET ROLE regress_lr_replication; SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding'); diff --git a/contrib/test_decoding/expected/slot.out b/contrib/test_decoding/expected/slot.out index 75b4b5cc625..217c45eae96 100644 --- a/contrib/test_decoding/expected/slot.out +++ b/contrib/test_decoding/expected/slot.out @@ -30,8 +30,11 @@ SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot_t2', 'tes init (1 row) +BEGIN; +SET LOCAL output_plugin_libraries = nonexistent; SELECT pg_create_logical_replication_slot('foo', 'nonexistent'); ERROR: could not access file "nonexistent": No such file or directory +ROLLBACK; -- here we want to start a new session and wait till old one is gone select pg_backend_pid() as oldpid \gset \c - diff --git a/contrib/test_decoding/sql/permissions.sql b/contrib/test_decoding/sql/permissions.sql index 312b5145937..fee36f5a096 100644 --- a/contrib/test_decoding/sql/permissions.sql +++ b/contrib/test_decoding/sql/permissions.sql @@ -15,6 +15,14 @@ SELECT data FROM pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'inc SELECT pg_drop_replication_slot('regression_slot'); RESET ROLE; +-- no users can load an untrusted plugin +SET output_plugin_libraries = pgoutput; +SET ROLE regress_lr_replication; +SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding'); +RESET ROLE; +SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding'); +RESET output_plugin_libraries; + -- replication user can control replication SET ROLE regress_lr_replication; SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'test_decoding'); diff --git a/contrib/test_decoding/sql/slot.sql b/contrib/test_decoding/sql/slot.sql index 6d83fb26782..0a1398726ee 100644 --- a/contrib/test_decoding/sql/slot.sql +++ b/contrib/test_decoding/sql/slot.sql @@ -9,7 +9,10 @@ SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot_p', 'test SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot_t2', 'test_decoding', true); +BEGIN; +SET LOCAL output_plugin_libraries = nonexistent; SELECT pg_create_logical_replication_slot('foo', 'nonexistent'); +ROLLBACK; -- here we want to start a new session and wait till old one is gone select pg_backend_pid() as oldpid \gset diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index cd625271ea8..b98194a624f 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -4132,6 +4132,63 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows + + output_plugin_libraries (string) + + output_plugin_libraries configuration parameter + + + + + Lists the libraries installed in + that are also trusted for use as logical output plugins by replication + clients. Any logical decoding + or replication requests for + other libraries will be refused. All users are subject to this + restriction. The default is 'pgoutput, test_decoding', + which are the two logical output plugins included in the standard + PostgreSQL distribution. + + + The format is a comma-separated list of library names, where each name + is interpreted as for the LOAD + command (but logical decoding clients must specify a plugin name that + exactly matches an entry in the list, without + variations in case or path structure). Whitespace between entries is + ignored; surround a library name with double quotes if you need to + include whitespace or commas in the name. + + + It is the responsibility of the server administrator to ensure that + libraries added to this list do not unintentionally give additional + privileges to non-superusers when they are loaded into the server. + + + + When updating the server from a version that does not have the + output_plugin_libraries parameter, the following + query can help construct the list of plugins that are required by all + persistent logical replication slots: + +SELECT DISTINCT plugin FROM pg_replication_slots WHERE plugin IS NOT NULL; + + Review the list carefully for safety before adjusting + output_plugin_libraries. + + + The above query can only display plugins which were successfully + added to replication slots at some point in the past. Newly refused + requests will appear in the logs with a message similar to + +ERROR: library "..." may not be used as an output plugin +DETAIL: The configuration parameter "output_plugin_libraries" (currently 'pgoutput, test_decoding') does not name this library as a trusted output plugin. +HINT: If it is safe for all REPLICATION users to use this library as an output plugin, add it to "output_plugin_libraries" and reload the server configuration. + + + + + + wal_keep_size (integer) diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml index 57e2d3e7573..873c1d5985d 100644 --- a/doc/src/sgml/logical-replication.sgml +++ b/doc/src/sgml/logical-replication.sgml @@ -556,6 +556,15 @@ and it must have the LOGIN attribute. + + The name of the output plugin used by the replication connection must be + included in the server's . (For + subscriptions, the plugin name that is used is pgoutput.) + Superusers may modify the trusted list per-connection, by including + options=-coutput_plugin_libraries=... in the connection + string. + + In order to be able to copy the initial table data, the role used for the replication connection must have the SELECT privilege on diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c index 39bcf844694..a7efca27512 100644 --- a/src/backend/replication/logical/logical.c +++ b/src/backend/replication/logical/logical.c @@ -41,7 +41,9 @@ #include "storage/proc.h" #include "storage/procarray.h" #include "utils/builtins.h" +#include "utils/guc.h" #include "utils/memutils.h" +#include "utils/varlena.h" /* data for errcontext callback */ typedef struct LogicalErrorCallbackState @@ -51,6 +53,9 @@ typedef struct LogicalErrorCallbackState XLogRecPtr report_location; } LogicalErrorCallbackState; +/* GUC variables */ +char *output_plugin_libraries_string; + /* wrappers around output plugin callbacks */ static void output_plugin_error_callback(void *arg); static void startup_cb_wrapper(LogicalDecodingContext *ctx, OutputPluginOptions *opt, @@ -175,7 +180,72 @@ StartupDecodingContext(List *output_plugin_options, * now. */ if (!fast_forward) - LoadOutputPlugin(&ctx->callbacks, NameStr(slot->data.plugin)); + { + /* + * Before loading this library, make sure it's been blessed for + * logical decoding. + */ + const char *plugin = NameStr(slot->data.plugin); + bool plugin_allowed = false; + + if (output_plugin_libraries_string && output_plugin_libraries_string[0]) + { + /* Check this plugin against output_plugin_libraries. */ + char *rawstring; + List *elemlist = NIL; + ListCell *lc; + + /* Need a modifiable copy */ + rawstring = pstrdup(output_plugin_libraries_string); + + if (!SplitGUCList(rawstring, ',', &elemlist)) + { + /* syntax error in list */ + ereport(LOG, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid list syntax in parameter \"%s\"", + "output_plugin_libraries"))); + + list_free(elemlist); + elemlist = NIL; + } + + foreach(lc, elemlist) + { + char *allowed = lfirst(lc); + + if (strcmp(allowed, plugin) == 0) + { + plugin_allowed = true; + break; + } + } + + list_free(elemlist); + pfree(rawstring); + } + + if (!plugin_allowed) + { + /* + * Use the same error message as check_restricted_library_name(), + * but provide additional context for the DBA in the logs. (The + * HINT will be sent to the client, but that's not a secret.) + */ + ereport(ERROR, + errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("library \"%s\" may not be used as an output plugin", + plugin), + /*- translator: The second %s is the value of the output_plugin_libraries GUC, which may contain whitespace, commas, and double-quotes (") */ + errdetail_log("The configuration parameter \"%s\" (currently '%s') does not name this library as a trusted output plugin.", + "output_plugin_libraries", + output_plugin_libraries_string), + errhint("If it is safe for all REPLICATION users to use this library as an output plugin, add it to \"%s\" and reload the server configuration.", + "output_plugin_libraries")); + } + + LoadOutputPlugin(&ctx->callbacks, plugin); + } /* * Now that the slot's xmin has been set, we can announce ourselves as a @@ -667,7 +737,9 @@ OutputPluginUpdateProgress(struct LogicalDecodingContext *ctx) /* * Load the output plugin, lookup its output plugin init function, and check - * that it provides the required callbacks. + * that it provides the required callbacks. The caller must have checked that + * the current user has the necessary privileges to load the given plugin; + * standard LOAD restrictions are not applied here. */ static void LoadOutputPlugin(OutputPluginCallbacks *callbacks, const char *plugin) diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 5ba6a886e52..26e0a311e89 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -74,6 +74,7 @@ #include "postmaster/postmaster.h" #include "postmaster/syslogger.h" #include "postmaster/walwriter.h" +#include "replication/logical.h" #include "replication/logicallauncher.h" #include "replication/reorderbuffer.h" #include "replication/slot.h" @@ -4613,6 +4614,17 @@ static struct config_string ConfigureNamesString[] = check_restrict_nonsystem_relation_kind, assign_restrict_nonsystem_relation_kind, NULL }, + { + {"output_plugin_libraries", PGC_SUSET, REPLICATION_SENDING, + gettext_noop("Lists libraries that may be named as logical decoding output plugins."), + gettext_noop("Users with REPLICATION privileges may only use plugins in this list when creating logical replication slots."), + GUC_LIST_INPUT | GUC_LIST_QUOTE | GUC_SUPERUSER_ONLY + }, + &output_plugin_libraries_string, + "pgoutput, test_decoding", + NULL, NULL, NULL + }, + /* End-of-list marker */ { {NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 618cdabcecc..a0a0c573927 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -727,6 +727,7 @@ #dynamic_library_path = '$libdir' #gin_fuzzy_search_limit = 0 +#output_plugin_libraries = 'pgoutput, test_decoding' # approved plugins for logical decoding #------------------------------------------------------------------------------ diff --git a/src/bin/pg_dump/dumputils.c b/src/bin/pg_dump/dumputils.c index 1e24e536f02..86a01e4391a 100644 --- a/src/bin/pg_dump/dumputils.c +++ b/src/bin/pg_dump/dumputils.c @@ -887,6 +887,7 @@ bool variable_is_guc_list_quote(const char *name) { if (pg_strcasecmp(name, "local_preload_libraries") == 0 || + pg_strcasecmp(name, "output_plugin_libraries") == 0 || pg_strcasecmp(name, "search_path") == 0 || pg_strcasecmp(name, "session_preload_libraries") == 0 || pg_strcasecmp(name, "shared_preload_libraries") == 0 || diff --git a/src/include/replication/logical.h b/src/include/replication/logical.h index daf458cf989..190bc8de0b1 100644 --- a/src/include/replication/logical.h +++ b/src/include/replication/logical.h @@ -140,4 +140,7 @@ extern bool filter_by_origin_cb_wrapper(LogicalDecodingContext *ctx, RepOriginId extern void ResetLogicalStreamingState(void); extern void UpdateDecodingStats(LogicalDecodingContext *ctx); +/* GUCs */ +extern PGDLLIMPORT char *output_plugin_libraries_string; + #endif diff --git a/src/test/subscription/t/100_bugs.pl b/src/test/subscription/t/100_bugs.pl index 81ff79fce46..d02cf2f2a2d 100644 --- a/src/test/subscription/t/100_bugs.pl +++ b/src/test/subscription/t/100_bugs.pl @@ -6,7 +6,7 @@ use strict; use warnings; use PostgresNode; use TestLib; -use Test::More tests => 12; +use Test::More tests => 14; # Bug #15114 @@ -363,6 +363,37 @@ is( $node_subscriber_d_cols->safe_psql( $node_publisher_d_cols->stop('fast'); $node_subscriber_d_cols->stop('fast'); +$node_publisher->rotate_logfile(); +$node_publisher->start(); + +my $publisher_host = $node_publisher->host; +my $publisher_port = $node_publisher->port; +my $connstr_db = + "host=$publisher_host port=$publisher_port replication=database dbname=postgres"; + +# REPLICATION users should not be able to bypass LOAD restrictions. +$node_publisher->safe_psql( + 'postgres', qq( + CREATE USER repluser REPLICATION; +)); + +my ($ret, $stdout, $stderr) = $node_publisher->psql( + 'postgres', + qq[ + SET ROLE repluser; + CREATE_REPLICATION_SLOT fail_slot LOGICAL regress;', + ], + timeout => $PostgreSQL::Test::Utils::timeout_default, + extra_params => [ '-d', $connstr_db ]); + +is($ret, 3, 'loading unblessed output plugin fails'); +like( + $stderr, + qr/ERROR: library "regress" may not be used as an output plugin/, + 'loading unblessed output plugin fails: stderr'); + +$node_publisher->stop('fast'); + # BUG #18988 # The bug happened due to a self-deadlock between the DROP SUBSCRIPTION # command and the walsender process for accessing pg_subscription. This @@ -380,7 +411,7 @@ $node_publisher->safe_psql( CREATE SUBSCRIPTION regress_sub1 CONNECTION '$publisher_connstr' PUBLICATION regress_pub WITH (connect=false); )); -my ($ret, $stdout, $stderr) = +($ret, $stdout, $stderr) = $node_publisher->psql('postgres', q{DROP SUBSCRIPTION regress_sub1}); isnt($ret, 0, "replication slot does not exist: exit code not 0");