From 93b52db71255b0ac10458a6b8f37bdc5e658f095 Mon Sep 17 00:00:00 2001
From: Greg Sabino Mullane <greg@turnstep.com>
Date: Fri, 21 Feb 2025 16:16:12 -0500
Subject: [PATCH] Add new server config cleartext_passwords_action

This controls what happens when someone sends a clear text password
to the server via CREATE USER or ALTER USER. Three states are allowed:

1. "warn" The current default, this issues a warning if a clear
text password is used, but allows the change to proceed. The hint
changes to recommend \password if the current application_name is 'psql'

2. "allow" This does nothing, and thus emulates the historical behavior.

3. "disallow". This prevents the use of plain text completely, by throwing
an error if a password set or change is attempted.
---
 .../passwordcheck/expected/passwordcheck.out  |  6 ++++
 doc/src/sgml/config.sgml                      | 18 ++++++++++
 src/backend/libpq/crypt.c                     | 34 +++++++++++++++++++
 src/backend/utils/misc/guc_tables.c           | 17 ++++++++++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/include/libpq/crypt.h                     | 19 +++++++++++
 .../ecpg/test/expected/connect-test5.stderr   | 16 ++++++---
 src/test/regress/expected/create_role.out     |  3 ++
 src/test/regress/expected/password.out        | 33 ++++++++++++++++++
 9 files changed, 143 insertions(+), 4 deletions(-)

diff --git a/contrib/passwordcheck/expected/passwordcheck.out b/contrib/passwordcheck/expected/passwordcheck.out
index 83472c76d27..602e91c50ac 100644
--- a/contrib/passwordcheck/expected/passwordcheck.out
+++ b/contrib/passwordcheck/expected/passwordcheck.out
@@ -3,6 +3,9 @@ LOAD 'passwordcheck';
 CREATE USER regress_passwordcheck_user1;
 -- ok
 ALTER USER regress_passwordcheck_user1 PASSWORD 'a_nice_long_password';
+WARNING:  using a clear text password
+DETAIL:  Sending a password using plain text is deprecated and may be removed in a future release of PostgreSQL.
+HINT:  Use a client that can change the password without sending it in clear text
 -- error: too short
 ALTER USER regress_passwordcheck_user1 PASSWORD 'tooshrt';
 ERROR:  password is too short
@@ -10,6 +13,9 @@ DETAIL:  password must be at least "passwordcheck.min_password_length" (8) bytes
 -- ok
 SET passwordcheck.min_password_length = 6;
 ALTER USER regress_passwordcheck_user1 PASSWORD 'v_shrt';
+WARNING:  using a clear text password
+DETAIL:  Sending a password using plain text is deprecated and may be removed in a future release of PostgreSQL.
+HINT:  Use a client that can change the password without sending it in clear text
 -- error: contains user name
 ALTER USER regress_passwordcheck_user1 PASSWORD 'xyzregress_passwordcheck_user1';
 ERROR:  password must not contain user name
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 007746a4429..fc37b4473ad 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1104,6 +1104,24 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-cleartext-passwords-action" xreflabel="cleartext_passwords_action">
+      <term><varname>cleartext_passwords_action</varname> (<type>enum</type>)
+      <indexterm>
+       <primary><varname>cleartext_passwords_action</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Controls what action to take when a password is sent unencrypted
+        (i.e. in clear text) to the server via the <command>CREATE ROLE</command>
+        or <command>ALTER ROLE</command> command. Valid options are
+        <literal>allow</literal>, <literal>warn</literal>, or
+        <literal>disallow</literal>.
+        The default value is <literal>warn</literal>.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-password-encryption" xreflabel="password_encryption">
       <term><varname>password_encryption</varname> (<type>enum</type>)
       <indexterm>
diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c
index cbb85a27cc1..b093aa9fef7 100644
--- a/src/backend/libpq/crypt.c
+++ b/src/backend/libpq/crypt.c
@@ -21,12 +21,16 @@
 #include "libpq/crypt.h"
 #include "libpq/scram.h"
 #include "utils/builtins.h"
+#include "utils/guc.h"
 #include "utils/syscache.h"
 #include "utils/timestamp.h"
 
 /* Enables deprecation warnings for MD5 passwords. */
 bool		md5_password_warnings = true;
 
+/* Action to take when clear text passwords are used. */
+int			cleartext_passwords_action = CLEARTEXT_ACTION_WARN;
+
 /*
  * Fetch stored password for a user, for authentication.
  *
@@ -131,6 +135,36 @@ encrypt_password(PasswordType target_type, const char *role,
 	}
 	else
 	{
+
+		/*
+		 * We are sending clear text passwords to the server. What should we
+		 * do about that?
+		 */
+		if (cleartext_passwords_action == CLEARTEXT_ACTION_WARN)
+		{
+			ereport(WARNING,
+					(errcode(ERRCODE_WARNING_DEPRECATED_FEATURE),
+					 errmsg("using a clear text password"),
+					 errdetail("Sending a password using plain text is deprecated and may be removed in a future release of PostgreSQL."),
+					 strncmp(application_name, "psql", 5) == 0
+					 ? errhint("If using psql, you can set the password with \\password")
+					 : errhint("Use a client that can change the password without sending it in clear text")));
+		}
+		else if (cleartext_passwords_action == CLEARTEXT_ACTION_DISALLOW)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PASSWORD),
+					 errmsg("using a clear text password"),
+					 errdetail("Sending a password using plain text is not allowed."),
+					 strncmp(application_name, "psql", 5) == 0
+					 ? errhint("If using psql, you can change the password with \\password")
+					 : errhint("Use a client that can change the password without sending it in clear text")));
+		}
+		else
+		{
+			/* Silently accept this bad practice. */
+		}
+
 		switch (target_type)
 		{
 			case PASSWORD_TYPE_MD5:
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 03a6dd49154..9288c23a507 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -409,6 +409,13 @@ static const struct config_enum_entry password_encryption_options[] = {
 	{NULL, 0, false}
 };
 
+static const struct config_enum_entry cleartext_action_options[] = {
+	{"allow", CLEARTEXT_ACTION_ALLOW, false},
+	{"warn", CLEARTEXT_ACTION_WARN, false},
+	{"disallow", CLEARTEXT_ACTION_DISALLOW, false},
+	{NULL, 0, false}
+};
+
 static const struct config_enum_entry ssl_protocol_versions_info[] = {
 	{"", PG_TLS_ANY, false},
 	{"TLSv1", PG_TLS1_VERSION, false},
@@ -5298,6 +5305,16 @@ struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"cleartext_passwords_action", PGC_SIGHUP, CONN_AUTH_AUTH,
+			gettext_noop("Action to take when clear text passwords are used."),
+		},
+		&cleartext_passwords_action,
+		CLEARTEXT_ACTION_WARN, cleartext_action_options,
+		NULL, NULL, NULL
+	},
+
+
 	/* End-of-list marker */
 	{
 		{NULL, 0, 0, NULL, NULL}, NULL, 0, NULL, NULL, NULL, NULL
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 5362ff80519..bea94e65d87 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -97,6 +97,7 @@
 #password_encryption = scram-sha-256	# scram-sha-256 or md5
 #scram_iterations = 4096
 #md5_password_warnings = on
+#cleartext_passwords_action = warn      # can be allow, warn, or disallow
 
 # GSSAPI using Kerberos
 #krb_server_keyfile = 'FILE:${sysconfdir}/krb5.keytab'
diff --git a/src/include/libpq/crypt.h b/src/include/libpq/crypt.h
index dee477428e4..d5d0d8af26e 100644
--- a/src/include/libpq/crypt.h
+++ b/src/include/libpq/crypt.h
@@ -28,6 +28,9 @@
 /* Enables deprecation warnings for MD5 passwords. */
 extern PGDLLIMPORT bool md5_password_warnings;
 
+/* Specifies action when clear text passwords are used. */
+extern PGDLLIMPORT int cleartext_passwords_action;
+
 /*
  * Types of password hashes or secrets.
  *
@@ -44,6 +47,22 @@ typedef enum PasswordType
 	PASSWORD_TYPE_SCRAM_SHA_256,
 } PasswordType;
 
+/*
+ * Actions to take when clear text passwords are used.
+ *
+ * Passwords that are sent in clear text via the CREATE/ALTER USER
+ * command can cause a reaction by the server. We can either allow
+ * (the old behavior), warn (throw a warning and hint), or simply
+ * disallow (throws an exception).
+ */
+typedef enum CleartextAction
+{
+	CLEARTEXT_ACTION_ALLOW = 0,
+	CLEARTEXT_ACTION_WARN,
+	CLEARTEXT_ACTION_DISALLOW,
+}			CleartextAction;
+
+
 extern PasswordType get_password_type(const char *shadow_pass);
 extern char *encrypt_password(PasswordType target_type, const char *role,
 							  const char *password);
diff --git a/src/interfaces/ecpg/test/expected/connect-test5.stderr b/src/interfaces/ecpg/test/expected/connect-test5.stderr
index 037db217586..d74cc1f1e81 100644
--- a/src/interfaces/ecpg/test/expected/connect-test5.stderr
+++ b/src/interfaces/ecpg/test/expected/connect-test5.stderr
@@ -4,16 +4,24 @@
 [NO_PID]: sqlca: code: 0, state: 00000
 [NO_PID]: ecpg_execute on line 24: query: alter user regress_ecpg_user2 encrypted password 'insecure'; with 0 parameter(s) on connection main
 [NO_PID]: sqlca: code: 0, state: 00000
-[NO_PID]: ecpg_execute on line 24: using PQexec
+[NO_PID]: ECPGnoticeReceiver: using a clear text password
 [NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: raising sqlcode 0
+[NO_PID]: sqlca: code: 0, state: 01P01
+[NO_PID]: ecpg_execute on line 24: using PQexec
+[NO_PID]: sqlca: code: 0, state: 01P01
 [NO_PID]: ecpg_process_output on line 24: OK: ALTER ROLE
-[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: sqlca: code: 0, state: 01P01
 [NO_PID]: ecpg_execute on line 25: query: alter user regress_ecpg_user1 encrypted password 'connectpw'; with 0 parameter(s) on connection main
 [NO_PID]: sqlca: code: 0, state: 00000
-[NO_PID]: ecpg_execute on line 25: using PQexec
+[NO_PID]: ECPGnoticeReceiver: using a clear text password
 [NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: raising sqlcode 0
+[NO_PID]: sqlca: code: 0, state: 01P01
+[NO_PID]: ecpg_execute on line 25: using PQexec
+[NO_PID]: sqlca: code: 0, state: 01P01
 [NO_PID]: ecpg_process_output on line 25: OK: ALTER ROLE
-[NO_PID]: sqlca: code: 0, state: 00000
+[NO_PID]: sqlca: code: 0, state: 01P01
 [NO_PID]: ECPGtrans on line 26: action "commit"; connection "main"
 [NO_PID]: sqlca: code: 0, state: 00000
 [NO_PID]: ecpg_finish: connection main closed
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 46d4f9efe99..8347205e4aa 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -64,6 +64,9 @@ CREATE ROLE regress_login LOGIN;
 CREATE ROLE regress_inherit INHERIT;
 CREATE ROLE regress_connection_limit CONNECTION LIMIT 5;
 CREATE ROLE regress_encrypted_password ENCRYPTED PASSWORD 'foo';
+WARNING:  using a clear text password
+DETAIL:  Sending a password using plain text is deprecated and may be removed in a future release of PostgreSQL.
+HINT:  Use a client that can change the password without sending it in clear text
 CREATE ROLE regress_password_null PASSWORD NULL;
 -- ok, backwards compatible noise words should be ignored
 CREATE ROLE regress_noiseword SYSID 12345;
diff --git a/src/test/regress/expected/password.out b/src/test/regress/expected/password.out
index 9bb3ab2818b..ec76afcbcef 100644
--- a/src/test/regress/expected/password.out
+++ b/src/test/regress/expected/password.out
@@ -14,16 +14,25 @@ SET password_encryption = 'scram-sha-256'; -- ok
 SET password_encryption = 'md5';
 CREATE ROLE regress_passwd1;
 ALTER ROLE regress_passwd1 PASSWORD 'role_pwd1';
+WARNING:  using a clear text password
+DETAIL:  Sending a password using plain text is deprecated and may be removed in a future release of PostgreSQL.
+HINT:  Use a client that can change the password without sending it in clear text
 WARNING:  setting an MD5-encrypted password
 DETAIL:  MD5 password support is deprecated and will be removed in a future release of PostgreSQL.
 HINT:  Refer to the PostgreSQL documentation for details about migrating to another password type.
 CREATE ROLE regress_passwd2;
 ALTER ROLE regress_passwd2 PASSWORD 'role_pwd2';
+WARNING:  using a clear text password
+DETAIL:  Sending a password using plain text is deprecated and may be removed in a future release of PostgreSQL.
+HINT:  Use a client that can change the password without sending it in clear text
 WARNING:  setting an MD5-encrypted password
 DETAIL:  MD5 password support is deprecated and will be removed in a future release of PostgreSQL.
 HINT:  Refer to the PostgreSQL documentation for details about migrating to another password type.
 SET password_encryption = 'scram-sha-256';
 CREATE ROLE regress_passwd3 PASSWORD 'role_pwd3';
+WARNING:  using a clear text password
+DETAIL:  Sending a password using plain text is deprecated and may be removed in a future release of PostgreSQL.
+HINT:  Use a client that can change the password without sending it in clear text
 CREATE ROLE regress_passwd4 PASSWORD NULL;
 -- check list of created entries
 --
@@ -63,6 +72,9 @@ ALTER ROLE regress_passwd2_new RENAME TO regress_passwd2;
 SET password_encryption = 'md5';
 -- encrypt with MD5
 ALTER ROLE regress_passwd2 PASSWORD 'foo';
+WARNING:  using a clear text password
+DETAIL:  Sending a password using plain text is deprecated and may be removed in a future release of PostgreSQL.
+HINT:  Use a client that can change the password without sending it in clear text
 WARNING:  setting an MD5-encrypted password
 DETAIL:  MD5 password support is deprecated and will be removed in a future release of PostgreSQL.
 HINT:  Refer to the PostgreSQL documentation for details about migrating to another password type.
@@ -75,6 +87,9 @@ ALTER ROLE regress_passwd3 PASSWORD 'SCRAM-SHA-256$4096:VLK4RMaQLCvNtQ==$6YtlR4t
 SET password_encryption = 'scram-sha-256';
 -- create SCRAM secret
 ALTER ROLE  regress_passwd4 PASSWORD 'foo';
+WARNING:  using a clear text password
+DETAIL:  Sending a password using plain text is deprecated and may be removed in a future release of PostgreSQL.
+HINT:  Use a client that can change the password without sending it in clear text
 -- already encrypted with MD5, use as it is
 CREATE ROLE regress_passwd5 PASSWORD 'md5e73a4b11df52a6068f8b39f90be36023';
 WARNING:  setting an MD5-encrypted password
@@ -83,15 +98,27 @@ HINT:  Refer to the PostgreSQL documentation for details about migrating to anot
 -- This looks like a valid SCRAM-SHA-256 secret, but it is not
 -- so it should be hashed with SCRAM-SHA-256.
 CREATE ROLE regress_passwd6 PASSWORD 'SCRAM-SHA-256$1234';
+WARNING:  using a clear text password
+DETAIL:  Sending a password using plain text is deprecated and may be removed in a future release of PostgreSQL.
+HINT:  Use a client that can change the password without sending it in clear text
 -- These may look like valid MD5 secrets, but they are not, so they
 -- should be hashed with SCRAM-SHA-256.
 -- trailing garbage at the end
 CREATE ROLE regress_passwd7 PASSWORD 'md5012345678901234567890123456789zz';
+WARNING:  using a clear text password
+DETAIL:  Sending a password using plain text is deprecated and may be removed in a future release of PostgreSQL.
+HINT:  Use a client that can change the password without sending it in clear text
 -- invalid length
 CREATE ROLE regress_passwd8 PASSWORD 'md501234567890123456789012345678901zz';
+WARNING:  using a clear text password
+DETAIL:  Sending a password using plain text is deprecated and may be removed in a future release of PostgreSQL.
+HINT:  Use a client that can change the password without sending it in clear text
 -- Changing the SCRAM iteration count
 SET scram_iterations = 1024;
 CREATE ROLE regress_passwd9 PASSWORD 'alterediterationcount';
+WARNING:  using a clear text password
+DETAIL:  Sending a password using plain text is deprecated and may be removed in a future release of PostgreSQL.
+HINT:  Use a client that can change the password without sending it in clear text
 SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+/=]+)\$([a-zA-Z0-9+=/]+):([a-zA-Z0-9+/=]+)', '\1$\2:<salt>$<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
     WHERE rolname LIKE 'regress_passwd%'
@@ -128,7 +155,13 @@ SELECT rolpassword FROM pg_authid WHERE rolname='regress_passwd_empty';
 -- stored/server keys. They will be re-hashed.
 CREATE ROLE regress_passwd_sha_len0 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941doaPOYg==$Ky+A30sewHIH3VHQLRN9vYsuzlgNyGNKCh37dy96Rqw=:COPdlNiIkrsacU5QoxydEuOH6e/KfiipeETb/bPw8ZI=';
 CREATE ROLE regress_passwd_sha_len1 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941doaPOYg==$Ky+A30sewHIH3VHQLRN9vYsuzlgNyGNKCh37dy96RqwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=:COPdlNiIkrsacU5QoxydEuOH6e/KfiipeETb/bPw8ZI=';
+WARNING:  using a clear text password
+DETAIL:  Sending a password using plain text is deprecated and may be removed in a future release of PostgreSQL.
+HINT:  Use a client that can change the password without sending it in clear text
 CREATE ROLE regress_passwd_sha_len2 PASSWORD 'SCRAM-SHA-256$4096:A6xHKoH/494E941doaPOYg==$Ky+A30sewHIH3VHQLRN9vYsuzlgNyGNKCh37dy96Rqw=:COPdlNiIkrsacU5QoxydEuOH6e/KfiipeETb/bPw8ZIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=';
+WARNING:  using a clear text password
+DETAIL:  Sending a password using plain text is deprecated and may be removed in a future release of PostgreSQL.
+HINT:  Use a client that can change the password without sending it in clear text
 -- Check that the invalid secrets were re-hashed. A re-hashed secret
 -- should not contain the original salt.
 SELECT rolname, rolpassword not like '%A6xHKoH/494E941doaPOYg==%' as is_rolpassword_rehashed
-- 
2.30.2

