Improving psql's \password command - Mailing list pgsql-hackers

From Tom Lane
Subject Improving psql's \password command
Date
Msg-id 747443.1635536754@sss.pgh.pa.us
Whole thread Raw
Responses Re: Improving psql's \password command
List pgsql-hackers
The attached patch responds to the discussion at [1].  That thread
pointed out that if you issue "\password" with no argument, psql tries
to set the password for the role identified by PQuser(conn), that is,
the originally requested login name.  That's problematic because
if you've done SET SESSION AUTHORIZATION or SET ROLE, you may no
longer have permissions to set the password for the original role.
Even if you do, that might not be what you were expecting, especially
since the psql documentation specifically says it sets the password
for "the current user".

The solution adopted here is to do "SELECT CURRENT_USER" to acquire
the correct role name, and to include the role name in the password
prompt so as to make it very clear which password is to be set.

While testing that, I noticed another bit of user-unfriendliness:
there's no obvious way to get out of it if you realize you are
setting the wrong user's password.  simple_prompt() ignores
control-C, and when you give up and press return, you'll just
get the prompt to enter the password again.  If at this point
you have the presence of mind to enter a deliberately different
string, you'll be out of the woods.  If you don't, and just hit
return again, you will get this response from the backend:

NOTICE:  empty string is not a valid password, clearing password

which is just about the worst default behavior I can think of.
If you're superuser, and you meant to set the password for user1
but typed user2 instead, you just clobbered user2's password,
and you have no easy way to undo that.

What I propose here is that we just refuse to let you set an
empty password this way:

postgres=# \password joe
Enter new password for user "joe":
Empty string is not a valid password.
postgres=#

If you actually want to clear the password, I don't see anything
wrong with entering "alter user joe password null" explicitly.

It's not clear to me whether we ought to back-patch some or all
of this.  In the other thread I expressed doubt about back-patching
security-relevant behavior changes, and that still seems like a
serious concern; but on the other hand, the behaviors enumerated
above are pretty bad.  Since \password is probably only used
interactively, it seems like we could get away with making these
changes: the addition of the user name to the prompt should be
enough to cue anyone about what's really going to happen.

A compromise position could be to keep PQuser() as the default
target role name in the back branches, but back-patch the other
aspects (the prompt addition and the exit on empty password).

Thoughts?

            regards, tom lane

[1] https://www.postgresql.org/message-id/flat/B340250F-A0E3-43BF-A1FB-2AE36003F68D%40gmail.com

diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 49d4c0e3ce..fac4b1e87f 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -2023,28 +2023,53 @@ exec_command_password(PsqlScanState scan_state, bool active_branch)

     if (active_branch)
     {
-        char       *opt0 = psql_scan_slash_option(scan_state,
+        char       *user = psql_scan_slash_option(scan_state,
                                                   OT_SQLID, NULL, true);
-        char       *pw1;
-        char       *pw2;
+        char       *pw1 = NULL;
+        char       *pw2 = NULL;
+        PQExpBufferData buf;

-        pw1 = simple_prompt("Enter new password: ", false);
-        pw2 = simple_prompt("Enter it again: ", false);
+        initPQExpBuffer(&buf);

-        if (strcmp(pw1, pw2) != 0)
+        if (user == NULL)
         {
-            pg_log_error("Passwords didn't match.");
-            success = false;
+            /* Fetch current user so we can report whose PW will be changed */
+            PGresult   *res;
+
+            res = PSQLexec("SELECT CURRENT_USER");
+            if (!res)
+                success = false;
+            else
+            {
+                user = pg_strdup(PQgetvalue(res, 0, 0));
+                PQclear(res);
+            }
         }
-        else
-        {
-            char       *user;
-            char       *encrypted_password;

-            if (opt0)
-                user = opt0;
+        if (success)
+        {
+            printfPQExpBuffer(&buf, _("Enter new password for user \"%s\": "),
+                              user);
+            pw1 = simple_prompt(buf.data, false);
+            if (*pw1 == '\0')
+            {
+                pg_log_error("Empty string is not a valid password.");
+                success = false;
+            }
             else
-                user = PQuser(pset.db);
+            {
+                pw2 = simple_prompt("Enter it again: ", false);
+                if (strcmp(pw1, pw2) != 0)
+                {
+                    pg_log_error("Passwords didn't match.");
+                    success = false;
+                }
+            }
+        }
+
+        if (success)
+        {
+            char       *encrypted_password;

             encrypted_password = PQencryptPasswordConn(pset.db, pw1, user, NULL);

@@ -2055,15 +2080,12 @@ exec_command_password(PsqlScanState scan_state, bool active_branch)
             }
             else
             {
-                PQExpBufferData buf;
                 PGresult   *res;

-                initPQExpBuffer(&buf);
                 printfPQExpBuffer(&buf, "ALTER USER %s PASSWORD ",
                                   fmtId(user));
                 appendStringLiteralConn(&buf, encrypted_password, pset.db);
                 res = PSQLexec(buf.data);
-                termPQExpBuffer(&buf);
                 if (!res)
                     success = false;
                 else
@@ -2072,10 +2094,13 @@ exec_command_password(PsqlScanState scan_state, bool active_branch)
             }
         }

-        if (opt0)
-            free(opt0);
-        free(pw1);
-        free(pw2);
+        if (user)
+            free(user);
+        if (pw1)
+            free(pw1);
+        if (pw2)
+            free(pw2);
+        termPQExpBuffer(&buf);
     }
     else
         ignore_slash_options(scan_state);

pgsql-hackers by date:

Previous
From: Bharath Rupireddy
Date:
Subject: add checkpoint stats of snapshot and mapping files of pg_logical dir
Next
From: Alexander Korotkov
Date:
Subject: Re: Add support for ALTER INDEX .. ALTER [COLUMN] col_num {SET,RESET}