From 56232f9879768e961485d8ba218da18c38768413 Mon Sep 17 00:00:00 2001
From: Noah Misch <noah@leadboat.com>
Date: Mon, 5 Oct 2015 10:06:29 -0400
Subject: [PATCH] pgcrypto: Detect and report too-short crypt() salts.

Certain short salts crashed the backend or disclosed a few bytes of
backend memory.  For existing salt-induced error conditions, emit a
message saying as much.  Back-patch to 9.0 (all supported versions).

Josh Kupershmidt

Security: CVE-2015-5288
---
 contrib/pgcrypto/crypt-blowfish.c            | 19 ++++++++++++++--
 contrib/pgcrypto/crypt-des.c                 | 22 +++++++++++++++---
 contrib/pgcrypto/expected/crypt-blowfish.out |  9 ++++++++
 contrib/pgcrypto/expected/crypt-des.out      |  4 ++++
 contrib/pgcrypto/expected/crypt-xdes.out     | 24 ++++++++++++++++++++
 contrib/pgcrypto/px-crypt.c                  |  2 +-
 contrib/pgcrypto/sql/crypt-blowfish.sql      |  9 ++++++++
 contrib/pgcrypto/sql/crypt-des.sql           |  4 ++++
 contrib/pgcrypto/sql/crypt-xdes.sql          | 16 +++++++++++++
 9 files changed, 103 insertions(+), 6 deletions(-)

diff --git a/contrib/pgcrypto/crypt-blowfish.c b/contrib/pgcrypto/crypt-blowfish.c
index fbaa3d776a0..4054e6a06fc 100644
--- a/contrib/pgcrypto/crypt-blowfish.c
+++ b/contrib/pgcrypto/crypt-blowfish.c
@@ -602,6 +602,17 @@ _crypt_blowfish_rn(const char *key, const char *setting,
 	if (size < 7 + 22 + 31 + 1)
 		return NULL;
 
+	/*
+	 * Blowfish salt value must be formatted as follows: "$2a$" or "$2x$", a
+	 * two digit cost parameter, "$", and 22 digits from the alphabet
+	 * "./0-9A-Za-z". -- from the PHP crypt docs. Apparently we enforce a few
+	 * more restrictions on the count in the salt as well.
+	 */
+	if (strlen(setting) < 29)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("invalid salt")));
+
 	if (setting[0] != '$' ||
 		setting[1] != '2' ||
 		(setting[2] != 'a' && setting[2] != 'x') ||
@@ -611,14 +622,18 @@ _crypt_blowfish_rn(const char *key, const char *setting,
 		(setting[4] == '3' && setting[5] > '1') ||
 		setting[6] != '$')
 	{
-		return NULL;
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("invalid salt")));
 	}
 
 	count = (BF_word) 1 << ((setting[4] - '0') * 10 + (setting[5] - '0'));
 	if (count < 16 || BF_decode(data.binary.salt, &setting[7], 16))
 	{
 		px_memset(data.binary.salt, 0, sizeof(data.binary.salt));
-		return NULL;
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("invalid salt")));
 	}
 	BF_swap(data.binary.salt, 4);
 
diff --git a/contrib/pgcrypto/crypt-des.c b/contrib/pgcrypto/crypt-des.c
index 4ed44beeff5..2108be8c379 100644
--- a/contrib/pgcrypto/crypt-des.c
+++ b/contrib/pgcrypto/crypt-des.c
@@ -681,9 +681,19 @@ px_crypt_des(const char *key, const char *setting)
 	if (*setting == _PASSWORD_EFMT1)
 	{
 		/*
-		 * "new"-style: setting - underscore, 4 bytes of count, 4 bytes of
-		 * salt key - unlimited characters
+		 * "new"-style: setting must be a 9-character (underscore, then 4
+		 * bytes of count, then 4 bytes of salt) string. See CRYPT(3) under
+		 * the "Extended crypt" heading for further details.
+		 *
+		 * Unlimited characters of the input key are used. This is known as
+		 * the "Extended crypt" DES method.
+		 *
 		 */
+		if (strlen(setting) < 9)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("invalid salt")));
+
 		for (i = 1, count = 0L; i < 5; i++)
 			count |= ascii_to_bin(setting[i]) << (i - 1) * 6;
 
@@ -723,10 +733,16 @@ px_crypt_des(const char *key, const char *setting)
 #endif   /* !DISABLE_XDES */
 	{
 		/*
-		 * "old"-style: setting - 2 bytes of salt key - up to 8 characters
+		 * "old"-style: setting - 2 bytes of salt key - only up to the first 8
+		 * characters of the input key are used.
 		 */
 		count = 25;
 
+		if (strlen(setting) < 2)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("invalid salt")));
+
 		salt = (ascii_to_bin(setting[1]) << 6)
 			| ascii_to_bin(setting[0]);
 
diff --git a/contrib/pgcrypto/expected/crypt-blowfish.out b/contrib/pgcrypto/expected/crypt-blowfish.out
index 329d78f6254..d79b0c047b4 100644
--- a/contrib/pgcrypto/expected/crypt-blowfish.out
+++ b/contrib/pgcrypto/expected/crypt-blowfish.out
@@ -13,6 +13,15 @@ SELECT crypt('foox', '$2a$06$RQiOJ.3ELirrXwxIZY8q0O');
  $2a$06$RQiOJ.3ELirrXwxIZY8q0OR3CVJrAfda1z26CCHPnB6mmVZD8p0/C
 (1 row)
 
+-- error, salt too short:
+SELECT crypt('foox', '$2a$');
+ERROR:  invalid salt
+-- error, first digit of count in salt invalid
+SELECT crypt('foox', '$2a$40$RQiOJ.3ELirrXwxIZY8q0O');
+ERROR:  invalid salt
+-- error, count in salt too small
+SELECT crypt('foox', '$2a$00$RQiOJ.3ELirrXwxIZY8q0O');
+ERROR:  invalid salt
 CREATE TABLE ctest (data text, res text, salt text);
 INSERT INTO ctest VALUES ('password', '', '');
 UPDATE ctest SET salt = gen_salt('bf', 8);
diff --git a/contrib/pgcrypto/expected/crypt-des.out b/contrib/pgcrypto/expected/crypt-des.out
index b8b605037d4..a462dcd580a 100644
--- a/contrib/pgcrypto/expected/crypt-des.out
+++ b/contrib/pgcrypto/expected/crypt-des.out
@@ -13,6 +13,10 @@ SELECT crypt('foox', 'NB');
  NB53EGGqrrb5E
 (1 row)
 
+-- We are supposed to pass in a 2-character salt.
+-- error since salt is too short:
+SELECT crypt('password', 'a');
+ERROR:  invalid salt
 CREATE TABLE ctest (data text, res text, salt text);
 INSERT INTO ctest VALUES ('password', '', '');
 UPDATE ctest SET salt = gen_salt('des');
diff --git a/contrib/pgcrypto/expected/crypt-xdes.out b/contrib/pgcrypto/expected/crypt-xdes.out
index cdcdefb1996..8cf907512f6 100644
--- a/contrib/pgcrypto/expected/crypt-xdes.out
+++ b/contrib/pgcrypto/expected/crypt-xdes.out
@@ -13,6 +13,30 @@ SELECT crypt('foox', '_J9..j2zz');
  _J9..j2zzAYKMvO2BYRY
 (1 row)
 
+-- check XDES handling of keys longer than 8 chars
+SELECT crypt('longlongpassword', '_J9..j2zz');
+        crypt         
+----------------------
+ _J9..j2zz4BeseiQNwUg
+(1 row)
+
+-- error, salt too short
+SELECT crypt('foox', '_J9..BWH');
+ERROR:  invalid salt
+-- error, count specified in the second argument is 0
+SELECT crypt('password', '_........');
+ERROR:  crypt(3) returned NULL
+-- error, count will wind up still being 0 due to invalid encoding
+-- of the count: only chars ``./0-9A-Za-z' are valid
+SELECT crypt('password', '_..!!!!!!');
+ERROR:  crypt(3) returned NULL
+-- count should be non-zero here, will work
+SELECT crypt('password', '_/!!!!!!!');
+        crypt         
+----------------------
+ _/!!!!!!!zqM49hRzxko
+(1 row)
+
 CREATE TABLE ctest (data text, res text, salt text);
 INSERT INTO ctest VALUES ('password', '', '');
 UPDATE ctest SET salt = gen_salt('xdes', 1001);
diff --git a/contrib/pgcrypto/px-crypt.c b/contrib/pgcrypto/px-crypt.c
index 7b003a76ca6..e3246fc5b9d 100644
--- a/contrib/pgcrypto/px-crypt.c
+++ b/contrib/pgcrypto/px-crypt.c
@@ -42,7 +42,7 @@ run_crypt_des(const char *psw, const char *salt,
 	char	   *res;
 
 	res = px_crypt_des(psw, salt);
-	if (strlen(res) > len - 1)
+	if (res == NULL || strlen(res) > len - 1)
 		return NULL;
 	strcpy(buf, res);
 	return buf;
diff --git a/contrib/pgcrypto/sql/crypt-blowfish.sql b/contrib/pgcrypto/sql/crypt-blowfish.sql
index 60c11400555..3b5a681c3f5 100644
--- a/contrib/pgcrypto/sql/crypt-blowfish.sql
+++ b/contrib/pgcrypto/sql/crypt-blowfish.sql
@@ -6,6 +6,15 @@ SELECT crypt('', '$2a$06$RQiOJ.3ELirrXwxIZY8q0O');
 
 SELECT crypt('foox', '$2a$06$RQiOJ.3ELirrXwxIZY8q0O');
 
+-- error, salt too short:
+SELECT crypt('foox', '$2a$');
+
+-- error, first digit of count in salt invalid
+SELECT crypt('foox', '$2a$40$RQiOJ.3ELirrXwxIZY8q0O');
+
+-- error, count in salt too small
+SELECT crypt('foox', '$2a$00$RQiOJ.3ELirrXwxIZY8q0O');
+
 CREATE TABLE ctest (data text, res text, salt text);
 INSERT INTO ctest VALUES ('password', '', '');
 
diff --git a/contrib/pgcrypto/sql/crypt-des.sql b/contrib/pgcrypto/sql/crypt-des.sql
index fabdc652fc9..a85ec1e6555 100644
--- a/contrib/pgcrypto/sql/crypt-des.sql
+++ b/contrib/pgcrypto/sql/crypt-des.sql
@@ -6,6 +6,10 @@ SELECT crypt('', 'NB');
 
 SELECT crypt('foox', 'NB');
 
+-- We are supposed to pass in a 2-character salt.
+-- error since salt is too short:
+SELECT crypt('password', 'a');
+
 CREATE TABLE ctest (data text, res text, salt text);
 INSERT INTO ctest VALUES ('password', '', '');
 
diff --git a/contrib/pgcrypto/sql/crypt-xdes.sql b/contrib/pgcrypto/sql/crypt-xdes.sql
index d4a74f76bde..8171cd872be 100644
--- a/contrib/pgcrypto/sql/crypt-xdes.sql
+++ b/contrib/pgcrypto/sql/crypt-xdes.sql
@@ -6,6 +6,22 @@ SELECT crypt('', '_J9..j2zz');
 
 SELECT crypt('foox', '_J9..j2zz');
 
+-- check XDES handling of keys longer than 8 chars
+SELECT crypt('longlongpassword', '_J9..j2zz');
+
+-- error, salt too short
+SELECT crypt('foox', '_J9..BWH');
+
+-- error, count specified in the second argument is 0
+SELECT crypt('password', '_........');
+
+-- error, count will wind up still being 0 due to invalid encoding
+-- of the count: only chars ``./0-9A-Za-z' are valid
+SELECT crypt('password', '_..!!!!!!');
+
+-- count should be non-zero here, will work
+SELECT crypt('password', '_/!!!!!!!');
+
 CREATE TABLE ctest (data text, res text, salt text);
 INSERT INTO ctest VALUES ('password', '', '');
 
-- 
GitLab