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");