Fix SQL injection in logical replication origin checks.

ALTER SUBSCRIPTION ... REFRESH PUBLICATION interpolates schema and
relation names into SQL without quoting them.  A crafted subscriber
relation name can inject arbitrary SQL on the publisher.  Test such a
name.  Back-patch to v16, where commit
8756930190 first appeared.

Reported-by: Pavel Kohout <pavel.kohout@aisle.com>
Author: Pavel Kohout <pavel.kohout@aisle.com>
Reviewed-by: Nathan Bossart <nathandbossart@gmail.com>
Backpatch-through: 16
Security: CVE-2026-6638
This commit is contained in:
Noah Misch 2026-05-11 05:13:50 -07:00
parent 00e27235e5
commit 248a433cd1
2 changed files with 26 additions and 18 deletions

View file

@ -1986,9 +1986,14 @@ check_publications_origin(WalReceiverConn *wrconn, List *publications,
Oid relid = subrel_local_oids[i];
char *schemaname = get_namespace_name(get_rel_namespace(relid));
char *tablename = get_rel_name(relid);
char *schemaname_lit = quote_literal_cstr(schemaname);
char *tablename_lit = quote_literal_cstr(tablename);
appendStringInfo(&cmd, "AND NOT (N.nspname = '%s' AND C.relname = '%s')\n",
schemaname, tablename);
appendStringInfo(&cmd, "AND NOT (N.nspname = %s AND C.relname = %s)\n",
schemaname_lit, tablename_lit);
pfree(schemaname_lit);
pfree(tablename_lit);
}
res = walrcv_exec(wrconn, cmd.data, 1, tableRow);

View file

@ -9,6 +9,9 @@ use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
use Test::More;
my $tab_unquoted = q{tab'le};
my $tab = qq{"$tab_unquoted"};
my $subname_AB = 'tap_sub_A_B';
my $subname_AB2 = 'tap_sub_A_B_2';
my $subname_BA = 'tap_sub_B_A';
@ -33,15 +36,15 @@ $node_B->init(allows_streaming => 'logical');
$node_B->start;
# Create table on node_A
$node_A->safe_psql('postgres', "CREATE TABLE tab (a int PRIMARY KEY)");
$node_A->safe_psql('postgres', "CREATE TABLE $tab (a int PRIMARY KEY)");
# Create the same table on node_B
$node_B->safe_psql('postgres', "CREATE TABLE tab (a int PRIMARY KEY)");
$node_B->safe_psql('postgres', "CREATE TABLE $tab (a int PRIMARY KEY)");
# Setup logical replication
# node_A (pub) -> node_B (sub)
my $node_A_connstr = $node_A->connstr . ' dbname=postgres';
$node_A->safe_psql('postgres', "CREATE PUBLICATION tap_pub_A FOR TABLE tab");
$node_A->safe_psql('postgres', "CREATE PUBLICATION tap_pub_A FOR TABLE $tab");
$node_B->safe_psql(
'postgres', "
CREATE SUBSCRIPTION $subname_BA
@ -51,7 +54,7 @@ $node_B->safe_psql(
# node_B (pub) -> node_A (sub)
my $node_B_connstr = $node_B->connstr . ' dbname=postgres';
$node_B->safe_psql('postgres', "CREATE PUBLICATION tap_pub_B FOR TABLE tab");
$node_B->safe_psql('postgres', "CREATE PUBLICATION tap_pub_B FOR TABLE $tab");
$node_A->safe_psql(
'postgres', "
CREATE SUBSCRIPTION $subname_AB
@ -71,25 +74,25 @@ is(1, 1, 'Bidirectional replication setup is complete');
###############################################################################
# insert a record
$node_A->safe_psql('postgres', "INSERT INTO tab VALUES (11);");
$node_B->safe_psql('postgres', "INSERT INTO tab VALUES (21);");
$node_A->safe_psql('postgres', "INSERT INTO $tab VALUES (11);");
$node_B->safe_psql('postgres', "INSERT INTO $tab VALUES (21);");
$node_A->wait_for_catchup($subname_BA);
$node_B->wait_for_catchup($subname_AB);
# check that transaction was committed on subscriber(s)
$result = $node_A->safe_psql('postgres', "SELECT * FROM tab ORDER BY 1;");
$result = $node_A->safe_psql('postgres', "SELECT * FROM $tab ORDER BY 1;");
is( $result, qq(11
21),
'Inserted successfully without leading to infinite recursion in bidirectional replication setup'
);
$result = $node_B->safe_psql('postgres', "SELECT * FROM tab ORDER BY 1;");
$result = $node_B->safe_psql('postgres', "SELECT * FROM $tab ORDER BY 1;");
is( $result, qq(11
21),
'Inserted successfully without leading to infinite recursion in bidirectional replication setup'
);
$node_A->safe_psql('postgres', "DELETE FROM tab;");
$node_A->safe_psql('postgres', "DELETE FROM $tab;");
$node_A->wait_for_catchup($subname_BA);
$node_B->wait_for_catchup($subname_AB);
@ -98,10 +101,10 @@ $node_B->wait_for_catchup($subname_AB);
# Check that remote data of node_B (that originated from node_C) is not
# published to node_A.
###############################################################################
$result = $node_A->safe_psql('postgres', "SELECT * FROM tab ORDER BY 1;");
$result = $node_A->safe_psql('postgres', "SELECT * FROM $tab ORDER BY 1;");
is($result, qq(), 'Check existing data');
$result = $node_B->safe_psql('postgres', "SELECT * FROM tab ORDER BY 1;");
$result = $node_B->safe_psql('postgres', "SELECT * FROM $tab ORDER BY 1;");
is($result, qq(), 'Check existing data');
# Initialize node node_C
@ -109,12 +112,12 @@ my $node_C = PostgreSQL::Test::Cluster->new('node_C');
$node_C->init(allows_streaming => 'logical');
$node_C->start;
$node_C->safe_psql('postgres', "CREATE TABLE tab (a int PRIMARY KEY)");
$node_C->safe_psql('postgres', "CREATE TABLE $tab (a int PRIMARY KEY)");
# Setup logical replication
# node_C (pub) -> node_B (sub)
my $node_C_connstr = $node_C->connstr . ' dbname=postgres';
$node_C->safe_psql('postgres', "CREATE PUBLICATION tap_pub_C FOR TABLE tab");
$node_C->safe_psql('postgres', "CREATE PUBLICATION tap_pub_C FOR TABLE $tab");
$node_B->safe_psql(
'postgres', "
CREATE SUBSCRIPTION $subname_BC
@ -124,17 +127,17 @@ $node_B->safe_psql(
$node_B->wait_for_subscription_sync($node_C, $subname_BC);
# insert a record
$node_C->safe_psql('postgres', "INSERT INTO tab VALUES (32);");
$node_C->safe_psql('postgres', "INSERT INTO $tab VALUES (32);");
$node_C->wait_for_catchup($subname_BC);
$node_B->wait_for_catchup($subname_AB);
$node_A->wait_for_catchup($subname_BA);
$result = $node_B->safe_psql('postgres', "SELECT * FROM tab ORDER BY 1;");
$result = $node_B->safe_psql('postgres', "SELECT * FROM $tab ORDER BY 1;");
is($result, qq(32), 'The node_C data replicated to node_B');
# check that the data published from node_C to node_B is not sent to node_A
$result = $node_A->safe_psql('postgres', "SELECT * FROM tab ORDER BY 1;");
$result = $node_A->safe_psql('postgres', "SELECT * FROM $tab ORDER BY 1;");
is($result, qq(),
'Remote data originating from another node (not the publisher) is not replicated when origin parameter is none'
);