]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Fix detection of passwords hashed with MD5 or SCRAM-SHA-256
authorMichael Paquier <michael@paquier.xyz>
Tue, 23 Apr 2019 06:43:32 +0000 (15:43 +0900)
committerMichael Paquier <michael@paquier.xyz>
Tue, 23 Apr 2019 06:43:32 +0000 (15:43 +0900)
This commit fixes a couple of issues related to the way password
verifiers hashed with MD5 or SCRAM-SHA-256 are detected, leading to
being able to store in catalogs passwords which do not follow the
supported hash formats:
- A MD5-hashed entry was checked based on if its header uses "md5" and
if the string length matches what is expected.  Unfortunately the code
never checked if the hash only used hexadecimal characters, as reported
by Tom Lane.
- A SCRAM-hashed entry was checked based on only its header, which
should be "SCRAM-SHA-256$", but it never checked for any fields
afterwards, as reported by Jonathan Katz.

Backpatch down to v10, which is where SCRAM has been introduced, and
where password verifiers in plain format have been removed.

Author: Jonathan Katz
Reviewed-by: Tom Lane, Michael Paquier
Discussion: https://postgr.es/m/016deb6b-1f0a-8e9f-1833-a8675b170aa9@postgresql.org
Backpatch-through: 10

src/backend/libpq/auth-scram.c
src/backend/libpq/crypt.c
src/include/common/md5.h
src/include/libpq/scram.h
src/test/regress/expected/password.out
src/test/regress/sql/password.sql

index e997c9460010585b1f89d58ce3c976301065d4c0..c110cb0720cb813faa4de80a1d599aab4ddd392a 100644 (file)
@@ -162,8 +162,6 @@ static char *build_server_first_message(scram_state *state);
 static char *build_server_final_message(scram_state *state);
 static bool verify_client_proof(scram_state *state);
 static bool verify_final_nonce(scram_state *state);
-static bool parse_scram_verifier(const char *verifier, int *iterations,
-                                        char **salt, uint8 *stored_key, uint8 *server_key);
 static void mock_scram_verifier(const char *username, int *iterations,
                                        char **salt, uint8 *stored_key, uint8 *server_key);
 static bool is_scram_printable(char *p);
@@ -547,7 +545,7 @@ scram_verify_plain_password(const char *username, const char *password,
  *
  * Returns true if the SCRAM verifier has been parsed, and false otherwise.
  */
-static bool
+bool
 parse_scram_verifier(const char *verifier, int *iterations, char **salt,
                                         uint8 *stored_key, uint8 *server_key)
 {
index 2c5ce4a47e167b9c25a773793583ef951313d866..0f19ed60f85f462f30d28f354c5e3b818926fcf3 100644 (file)
@@ -20,6 +20,7 @@
 
 #include "catalog/pg_authid.h"
 #include "common/md5.h"
+#include "common/scram-common.h"
 #include "libpq/crypt.h"
 #include "libpq/scram.h"
 #include "miscadmin.h"
@@ -90,9 +91,17 @@ get_role_password(const char *role, char **logdetail)
 PasswordType
 get_password_type(const char *shadow_pass)
 {
-       if (strncmp(shadow_pass, "md5", 3) == 0 && strlen(shadow_pass) == MD5_PASSWD_LEN)
+       char       *encoded_salt;
+       int                     iterations;
+       uint8           stored_key[SCRAM_KEY_LEN];
+       uint8           server_key[SCRAM_KEY_LEN];
+
+       if (strncmp(shadow_pass, "md5", 3) == 0 &&
+               strlen(shadow_pass) == MD5_PASSWD_LEN &&
+               strspn(shadow_pass + 3, MD5_PASSWD_CHARSET) == MD5_PASSWD_LEN - 3)
                return PASSWORD_TYPE_MD5;
-       if (strncmp(shadow_pass, "SCRAM-SHA-256$", strlen("SCRAM-SHA-256$")) == 0)
+       if (parse_scram_verifier(shadow_pass, &iterations, &encoded_salt,
+                                                        stored_key, server_key))
                return PASSWORD_TYPE_SCRAM_SHA_256;
        return PASSWORD_TYPE_PLAINTEXT;
 }
index 905d3aa2197305036563fac1041ddf8fb72d5eda..f418135a9a6ddf3bb40d46e0d999597937883307 100644 (file)
@@ -16,6 +16,7 @@
 #ifndef PG_MD5_H
 #define PG_MD5_H
 
+#define MD5_PASSWD_CHARSET     "0123456789abcdef"
 #define MD5_PASSWD_LEN 35
 
 extern bool pg_md5_hash(const void *buff, size_t len, char *hexsum);
index f7865ca5fc131692aa9311c2ef542979a6cb9022..7e5dd1412784053539ae11265162d57ad663ab7c 100644 (file)
@@ -29,6 +29,8 @@ extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
 
 /* Routines to handle and check SCRAM-SHA-256 verifier */
 extern char *pg_be_scram_build_verifier(const char *password);
+extern bool parse_scram_verifier(const char *verifier, int *iterations, char **salt,
+                                        uint8 *stored_key, uint8 *server_key);
 extern bool scram_verify_plain_password(const char *username,
                                                        const char *password, const char *verifier);
 
index 393d836eada1cdf4b30892ef73ea2ab374a65b2e..971e290a3210a6f0ec6c0d01f9ad70e9a65a0e77 100644 (file)
@@ -62,6 +62,15 @@ SET password_encryption = 'scram-sha-256';
 ALTER ROLE  regress_passwd4 PASSWORD 'foo';
 -- already encrypted with MD5, use as it is
 CREATE ROLE regress_passwd5 PASSWORD 'md5e73a4b11df52a6068f8b39f90be36023';
+-- This looks like a valid SCRAM-SHA-256 verifier, but it is not
+-- so it should be hashed with SCRAM-SHA-256.
+CREATE ROLE regress_passwd6 PASSWORD 'SCRAM-SHA-256$1234';
+-- These may look like valid MD5 verifiers, but they are not, so they
+-- should be hashed with SCRAM-SHA-256.
+-- trailing garbage at the end
+CREATE ROLE regress_passwd7 PASSWORD 'md5012345678901234567890123456789zz';
+-- invalid length
+CREATE ROLE regress_passwd8 PASSWORD 'md501234567890123456789012345678901zz';
 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%'
@@ -73,7 +82,10 @@ SELECT rolname, regexp_replace(rolpassword, '(SCRAM-SHA-256)\$(\d+):([a-zA-Z0-9+
  regress_passwd3 | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey>
  regress_passwd4 | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey>
  regress_passwd5 | md5e73a4b11df52a6068f8b39f90be36023
-(5 rows)
+ regress_passwd6 | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey>
+ regress_passwd7 | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey>
+ regress_passwd8 | SCRAM-SHA-256$4096:<salt>$<storedkey>:<serverkey>
+(8 rows)
 
 -- An empty password is not allowed, in any form
 CREATE ROLE regress_passwd_empty PASSWORD '';
@@ -93,6 +105,9 @@ DROP ROLE regress_passwd2;
 DROP ROLE regress_passwd3;
 DROP ROLE regress_passwd4;
 DROP ROLE regress_passwd5;
+DROP ROLE regress_passwd6;
+DROP ROLE regress_passwd7;
+DROP ROLE regress_passwd8;
 DROP ROLE regress_passwd_empty;
 -- all entries should have been removed
 SELECT rolname, rolpassword
index 8f8252d127f0bae3f063d0de5178cdedf873e22c..89b6d4b278dfaa162bb89be475151b6523ec1e4e 100644 (file)
@@ -54,6 +54,16 @@ ALTER ROLE  regress_passwd4 PASSWORD 'foo';
 -- already encrypted with MD5, use as it is
 CREATE ROLE regress_passwd5 PASSWORD 'md5e73a4b11df52a6068f8b39f90be36023';
 
+-- This looks like a valid SCRAM-SHA-256 verifier, but it is not
+-- so it should be hashed with SCRAM-SHA-256.
+CREATE ROLE regress_passwd6 PASSWORD 'SCRAM-SHA-256$1234';
+-- These may look like valid MD5 verifiers, but they are not, so they
+-- should be hashed with SCRAM-SHA-256.
+-- trailing garbage at the end
+CREATE ROLE regress_passwd7 PASSWORD 'md5012345678901234567890123456789zz';
+-- invalid length
+CREATE ROLE regress_passwd8 PASSWORD 'md501234567890123456789012345678901zz';
+
 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%'
@@ -70,6 +80,9 @@ DROP ROLE regress_passwd2;
 DROP ROLE regress_passwd3;
 DROP ROLE regress_passwd4;
 DROP ROLE regress_passwd5;
+DROP ROLE regress_passwd6;
+DROP ROLE regress_passwd7;
+DROP ROLE regress_passwd8;
 DROP ROLE regress_passwd_empty;
 
 -- all entries should have been removed