From: Noah Misch Date: Mon, 11 May 2026 12:13:46 +0000 (-0700) Subject: Fix SQL injection in logical replication origin checks. X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=46b4f5c11b0f2f6e6db834ffafc0d1a01a0373c1;p=thirdparty%2Fpostgresql.git 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 875693019053b8897ec3983e292acbb439b088c3 first appeared. Reported-by: Pavel Kohout Author: Pavel Kohout Reviewed-by: Nathan Bossart Backpatch-through: 16 Security: CVE-2026-6638 --- diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c index 1e10d9d9a58..7818f667edf 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -2775,9 +2775,14 @@ check_publications_origin_tables(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); } } @@ -2897,10 +2902,15 @@ check_publications_origin_sequences(WalReceiverConn *wrconn, List *publications, Oid relid = subrel_local_oids[i]; char *schemaname = get_namespace_name(get_rel_namespace(relid)); char *seqname = get_rel_name(relid); + char *schemaname_lit = quote_literal_cstr(schemaname); + char *seqname_lit = quote_literal_cstr(seqname); appendStringInfo(&cmd, - "AND NOT (N.nspname = '%s' AND C.relname = '%s')\n", - schemaname, seqname); + "AND NOT (N.nspname = %s AND C.relname = %s)\n", + schemaname_lit, seqname_lit); + + pfree(schemaname_lit); + pfree(seqname_lit); } res = walrcv_exec(wrconn, cmd.data, 1, tableRow); diff --git a/src/test/subscription/t/030_origin.pl b/src/test/subscription/t/030_origin.pl index 5076ebe609b..6bc6b7874c2 100644 --- a/src/test/subscription/t/030_origin.pl +++ b/src/test/subscription/t/030_origin.pl @@ -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'; @@ -38,15 +41,15 @@ $node_B->append_conf('postgresql.conf', 'track_commit_timestamp = on'); $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 @@ -56,7 +59,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 @@ -76,25 +79,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); @@ -103,10 +106,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 @@ -114,12 +117,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 @@ -129,17 +132,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' ); @@ -149,37 +152,37 @@ is($result, qq(), # delete a row that was previously modified by a different source. ############################################################################### -$node_B->safe_psql('postgres', "DELETE FROM tab;"); +$node_B->safe_psql('postgres', "DELETE FROM $tab;"); -$node_A->safe_psql('postgres', "INSERT INTO tab VALUES (32);"); +$node_A->safe_psql('postgres', "INSERT INTO $tab VALUES (32);"); $node_A->wait_for_catchup($subname_BA); $node_B->wait_for_catchup($subname_AB); -$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_A data replicated to node_B'); # The update should update the row on node B that was inserted by node A. -$node_C->safe_psql('postgres', "UPDATE tab SET a = 33 WHERE a = 32;"); +$node_C->safe_psql('postgres', "UPDATE $tab SET a = 33 WHERE a = 32;"); $node_B->wait_for_log( - qr/conflict detected on relation "public.tab": conflict=update_origin_differs.*\n.*DETAIL:.* Updating the row that was modified by a different origin ".*" in transaction [0-9]+ at .*: local row \(32\), remote row \(33\), replica identity \(a\)=\(32\)./ + qr/conflict detected on relation "public.$tab_unquoted": conflict=update_origin_differs.*\n.*DETAIL:.* Updating the row that was modified by a different origin ".*" in transaction [0-9]+ at .*: local row \(32\), remote row \(33\), replica identity \(a\)=\(32\)./ ); -$node_B->safe_psql('postgres', "DELETE FROM tab;"); -$node_A->safe_psql('postgres', "INSERT INTO tab VALUES (33);"); +$node_B->safe_psql('postgres', "DELETE FROM $tab;"); +$node_A->safe_psql('postgres', "INSERT INTO $tab VALUES (33);"); $node_A->wait_for_catchup($subname_BA); $node_B->wait_for_catchup($subname_AB); -$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(33), 'The node_A data replicated to node_B'); # The delete should remove the row on node B that was inserted by node A. -$node_C->safe_psql('postgres', "DELETE FROM tab WHERE a = 33;"); +$node_C->safe_psql('postgres', "DELETE FROM $tab WHERE a = 33;"); $node_B->wait_for_log( - qr/conflict detected on relation "public.tab": conflict=delete_origin_differs.*\n.*DETAIL:.* Deleting the row that was modified by a different origin ".*" in transaction [0-9]+ at .*: local row \(33\), replica identity \(a\)=\(33\).*/ + qr/conflict detected on relation "public.$tab_unquoted": conflict=delete_origin_differs.*\n.*DETAIL:.* Deleting the row that was modified by a different origin ".*" in transaction [0-9]+ at .*: local row \(33\), replica identity \(a\)=\(33\).*/ ); # The remaining tests no longer test conflict detection.