From a8bbbe75034979f15a56815127a2d6439eaecd36 Mon Sep 17 00:00:00 2001 From: Matheus Alcantara Date: Wed, 19 Mar 2025 14:16:18 -0300 Subject: [PATCH v9 3/3] postgres_fdw: improve security checks On adding scram pass-through for dblink [1] thread, it was discussed that we should not by-pass fdw security check as it was implemented for postgres_fdw on 761c79508e7. This commit improve the security check by adding new scram pass-through checks to ensure that the required scram connection options are not overwritten by the user mapping or foreign server options. [1] https://www.postgresql.org/message-id/flat/CAFY6G8ercA1KES%3DE_0__R9QCTR805TTyYr1No8qF8ZxmMg8z2Q%40mail.gmail.com --- contrib/postgres_fdw/connection.c | 100 ++++++++++++++++++++--- contrib/postgres_fdw/t/001_auth_scram.pl | 44 +++++++++- doc/src/sgml/postgres-fdw.sgml | 15 +--- 3 files changed, 133 insertions(+), 26 deletions(-) diff --git a/contrib/postgres_fdw/connection.c b/contrib/postgres_fdw/connection.c index 8ef9702c05c..b8b30284086 100644 --- a/contrib/postgres_fdw/connection.c +++ b/contrib/postgres_fdw/connection.c @@ -185,6 +185,8 @@ static void postgres_fdw_get_connections_internal(FunctionCallInfo fcinfo, static int pgfdw_conn_check(PGconn *conn); static bool pgfdw_conn_checkable(void); +static bool pgfdw_has_required_scram_options(const char **keywords, const char **values); + /* * Get a PGconn which can be used to execute queries on the remote PostgreSQL * server with the user's authorization. A new connection is established @@ -455,6 +457,15 @@ pgfdw_security_check(const char **keywords, const char **values, UserMapping *us } } + /* + * Ok if SCRAM pass-through is being used and all required scram options + * are set correctly. If pgfdw_has_required_scram_options returns true we + * assume that UseScramPassthrough is also true since SCRAM options are + * only set when UseScramPassthrough is enabled. + */ + if (MyProcPort->has_scram_keys && pgfdw_has_required_scram_options(keywords, values)) + return; + ereport(ERROR, (errcode(ERRCODE_S_R_E_PROHIBITED_SQL_STATEMENT_ATTEMPTED), errmsg("password or GSSAPI delegated credentials required"), @@ -556,6 +567,7 @@ connect_pg_server(ForeignServer *server, UserMapping *user) values[n] = GetDatabaseEncodingName(); n++; + /* Add required SCRAM pass-through connection options if it's enable. */ if (MyProcPort->has_scram_keys && UseScramPassthrough(server, user)) { int len; @@ -582,16 +594,20 @@ connect_pg_server(ForeignServer *server, UserMapping *user) if (encoded_len < 0) elog(ERROR, "could not encode SCRAM server key"); n++; + + /* + * Require scram-sha-256 to ensure that no other auth method is + * used when connecting with foreign server. + */ + keywords[n] = "require_auth"; + values[n] = "scram-sha-256"; + n++; } keywords[n] = values[n] = NULL; - /* - * Verify the set of connection parameters only if scram pass-through - * is not being used because the password is not necessary. - */ - if (!(MyProcPort->has_scram_keys && UseScramPassthrough(server, user))) - check_conn_params(keywords, values, user); + /* Verify the set of connection parameters. */ + check_conn_params(keywords, values, user); /* first time, allocate or get the custom wait event */ if (pgfdw_we_connect == 0) @@ -609,12 +625,8 @@ connect_pg_server(ForeignServer *server, UserMapping *user) server->servername), errdetail_internal("%s", pchomp(PQerrorMessage(conn))))); - /* - * Perform post-connection security checks only if scram pass-through - * is not being used because the password is not necessary. - */ - if (!(MyProcPort->has_scram_keys && UseScramPassthrough(server, user))) - pgfdw_security_check(keywords, values, user, conn); + /* Perform post-connection security checks. */ + pgfdw_security_check(keywords, values, user, conn); /* Prepare new session for use */ configure_remote_session(conn); @@ -725,6 +737,15 @@ check_conn_params(const char **keywords, const char **values, UserMapping *user) if (!UserMappingPasswordRequired(user)) return; + /* + * Ok if SCRAM pass-through is being used and all required scram options + * are set correctly. If pgfdw_has_required_scram_options returns true we + * assume that UseScramPassthrough is also true since SCRAM options are + * only set when UseScramPassthrough is enabled. + */ + if (MyProcPort->has_scram_keys && pgfdw_has_required_scram_options(keywords, values)) + return; + ereport(ERROR, (errcode(ERRCODE_S_R_E_PROHIBITED_SQL_STATEMENT_ATTEMPTED), errmsg("password or GSSAPI delegated credentials required"), @@ -2487,3 +2508,58 @@ pgfdw_conn_checkable(void) return false; #endif } + + /* + * Ensure that require_auth and scram keys are correctly set on values. SCRAM + * keys used to pass-through are coming from the initial connection from the + * client with the server. + * + * All required scram options are set by postgres_fdw, so we just need to + * ensure that these options are not overwritten by the user. + */ +static bool +pgfdw_has_required_scram_options(const char **keywords, const char **values) +{ + int i; + bool has_scram_server_key = false; + bool has_scram_client_key = false; + bool has_require_auth = false; + bool has_scram_keys = false; + + /* + * Continue iterating even if we found the keys that we need to validate + * to make sure that there is no other declaration of these keys that can + * overwrite the first. + */ + for (i = 0; keywords[i] != NULL; i++) + { + if (strcmp(keywords[i], "scram_client_key") == 0) + { + if (values[i] != NULL && values[i][0] != '\0') + has_scram_client_key = true; + else + has_scram_client_key = false; + } + + if (strcmp(keywords[i], "scram_server_key") == 0) + { + if (values[i] != NULL && values[i][0] != '\0') + has_scram_server_key = true; + else + has_scram_server_key = false; + } + + if (strcmp(keywords[i], "require_auth") == 0) + { + if (values[i] != NULL && strcmp(values[i], "scram-sha-256") == 0) + has_require_auth = true; + else + has_require_auth = false; + } + } + + has_scram_keys = has_scram_client_key && has_scram_server_key && MyProcPort->has_scram_keys; + + + return (has_scram_keys && has_require_auth); +} diff --git a/contrib/postgres_fdw/t/001_auth_scram.pl b/contrib/postgres_fdw/t/001_auth_scram.pl index 047840cc914..c45cbe6946c 100644 --- a/contrib/postgres_fdw/t/001_auth_scram.pl +++ b/contrib/postgres_fdw/t/001_auth_scram.pl @@ -68,6 +68,47 @@ test_fdw_auth($node1, $db0, "t2", $fdw_server2, test_auth($node2, $db2, "t2", "SCRAM auth directly on foreign server should still succeed"); +# Ensure that trust connections fail without superuser opt-in. +unlink($node1->data_dir . '/pg_hba.conf'); +unlink($node2->data_dir . '/pg_hba.conf'); + +$node1->append_conf( + 'pg_hba.conf', qq{ +local db0 all scram-sha-256 +local db1 all trust +} +); +$node2->append_conf( + 'pg_hba.conf', qq{ +local all all password +} +); + +$node1->restart; +$node2->restart; + +my ($ret, $stdout, $stderr) = $node1->psql( + $db0, + qq'select count(1) from t', + connstr => $node1->connstr($db0) . " user=$user"); + +is($ret, 3, 'loopback trust fails on the same cluster'); +like( + $stderr, + qr/failed: authentication method requirement "scram-sha-256"/, + 'expected error from loopback trust (same cluster)'); + +($ret, $stdout, $stderr) = $node1->psql( + $db0, + qq'select count(1) from t2', + connstr => $node1->connstr($db0) . " user=$user"); + +is($ret, 3, 'loopback password fails on a different cluster'); +like( + $stderr, + qr/failed: authentication method requirement "scram-sha-256"/, + 'expected error from loopback password (different cluster)'); + # Helper functions sub test_auth @@ -109,7 +150,8 @@ sub setup_pghba 'pg_hba.conf', qq{ local all all scram-sha-256 host all all $hostaddr/32 scram-sha-256 - }); + } + ); $node->restart; } diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml index a7f2f5ca182..c4d0f5ed2a3 100644 --- a/doc/src/sgml/postgres-fdw.sgml +++ b/doc/src/sgml/postgres-fdw.sgml @@ -788,10 +788,8 @@ OPTIONS (ADD password_required 'false'); - The remote server must request SCRAM authentication. (If desired, - enforce this on the client side (FDW side) with the option - require_auth.) If another authentication method - is requested by the server, then that one will be used normally. + The remote server must request the scram-sha-256 + authentication method; otherwise, the connection will fail. @@ -803,15 +801,6 @@ OPTIONS (ADD password_required 'false'); - - - The user mapping password is not used. (It could be set to support - other authentication methods, but that would arguably violate the - point of this feature, which is to avoid storing plain-text - passwords.) - - - The server running postgres_fdw and the remote -- 2.39.5 (Apple Git-154)