]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Fix SQL injection in logical replication origin checks.
authorNoah Misch <noah@leadboat.com>
Mon, 11 May 2026 12:13:46 +0000 (05:13 -0700)
committerNoah Misch <noah@leadboat.com>
Mon, 11 May 2026 12:13:46 +0000 (05:13 -0700)
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 <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

src/backend/commands/subscriptioncmds.c
src/test/subscription/t/030_origin.pl

index 1e10d9d9a58c33037c4939827f30a390f05976d3..7818f667edfa1576911f720af0f28268c6a1ed47 100644 (file)
@@ -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);
index 5076ebe609bb9e04f6b62909edcdcb5216b69b3e..6bc6b7874c20a7ac42e41d8376e27b4fe0638b5e 100644 (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';
@@ -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.