From c742b795ddce852975c513a04c29966420937553 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Wed, 18 Nov 2009 21:57:56 +0000 Subject: [PATCH] Add a hook to CREATE/ALTER ROLE to allow an external module to check the strength of database passwords, and create a sample implementation of such a hook as a new contrib module "passwordcheck". Laurenz Albe, reviewed by Takahiro Itagaki --- contrib/Makefile | 3 +- contrib/README | 4 + contrib/passwordcheck/Makefile | 19 ++++ contrib/passwordcheck/passwordcheck.c | 147 ++++++++++++++++++++++++++ doc/src/sgml/contrib.sgml | 3 +- doc/src/sgml/filelist.sgml | 3 +- doc/src/sgml/passwordcheck.sgml | 62 +++++++++++ src/backend/commands/user.c | 85 +++++++++++---- src/include/commands/user.h | 10 +- 9 files changed, 313 insertions(+), 23 deletions(-) create mode 100644 contrib/passwordcheck/Makefile create mode 100644 contrib/passwordcheck/passwordcheck.c create mode 100644 doc/src/sgml/passwordcheck.sgml diff --git a/contrib/Makefile b/contrib/Makefile index 8543b5287f..0b208851c1 100644 --- a/contrib/Makefile +++ b/contrib/Makefile @@ -1,4 +1,4 @@ -# $PostgreSQL: pgsql/contrib/Makefile,v 1.89 2009/08/18 10:34:39 teodor Exp $ +# $PostgreSQL: pgsql/contrib/Makefile,v 1.90 2009/11/18 21:57:56 tgl Exp $ subdir = contrib top_builddir = .. @@ -25,6 +25,7 @@ SUBDIRS = \ ltree \ oid2name \ pageinspect \ + passwordcheck \ pg_buffercache \ pg_freespacemap \ pg_standby \ diff --git a/contrib/README b/contrib/README index a8396a5bfa..ff35c08a70 100644 --- a/contrib/README +++ b/contrib/README @@ -104,6 +104,10 @@ pageinspect - Allows inspection of database pages Heikki Linnakangas +passwordcheck - + Simple password strength checker + Laurenz Albe + pg_buffercache - Real time queries on the shared buffer cache by Mark Kirkwood diff --git a/contrib/passwordcheck/Makefile b/contrib/passwordcheck/Makefile new file mode 100644 index 0000000000..1d2c8b1c34 --- /dev/null +++ b/contrib/passwordcheck/Makefile @@ -0,0 +1,19 @@ +# $PostgreSQL: pgsql/contrib/passwordcheck/Makefile,v 1.1 2009/11/18 21:57:56 tgl Exp $ + +MODULE_big = passwordcheck +OBJS = passwordcheck.o + +# uncomment the following two lines to enable cracklib support +# PG_CPPFLAGS = -DUSE_CRACKLIB '-DCRACKLIB_DICTPATH="/usr/lib/cracklib_dict"' +# SHLIB_LINK = -lcrack + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = contrib/passwordcheck +top_builddir = ../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/contrib/passwordcheck/passwordcheck.c b/contrib/passwordcheck/passwordcheck.c new file mode 100644 index 0000000000..88055e374d --- /dev/null +++ b/contrib/passwordcheck/passwordcheck.c @@ -0,0 +1,147 @@ +/*------------------------------------------------------------------------- + * + * passwordcheck.c + * + * + * Copyright (c) 2009, PostgreSQL Global Development Group + * + * Author: Laurenz Albe + * + * IDENTIFICATION + * $PostgreSQL: pgsql/contrib/passwordcheck/passwordcheck.c,v 1.1 2009/11/18 21:57:56 tgl Exp $ + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include + +#ifdef USE_CRACKLIB +#include +#endif + +#include "commands/user.h" +#include "fmgr.h" +#include "libpq/md5.h" + + +PG_MODULE_MAGIC; + +/* passwords shorter than this will be rejected */ +#define MIN_PWD_LENGTH 8 + +extern void _PG_init(void); + +/* + * check_password + * + * performs checks on an encrypted or unencrypted password + * ereport's if not acceptable + * + * username: name of role being created or changed + * password: new password (possibly already encrypted) + * password_type: PASSWORD_TYPE_PLAINTEXT or PASSWORD_TYPE_MD5 (there + * could be other encryption schemes in future) + * validuntil_time: password expiration time, as a timestamptz Datum + * validuntil_null: true if password expiration time is NULL + * + * This sample implementation doesn't pay any attention to the password + * expiration time, but you might wish to insist that it be non-null and + * not too far in the future. + */ +static void +check_password(const char *username, + const char *password, + int password_type, + Datum validuntil_time, + bool validuntil_null) +{ + int namelen = strlen(username); + int pwdlen = strlen(password); + char encrypted[MD5_PASSWD_LEN + 1]; + int i; + bool pwd_has_letter, + pwd_has_nonletter; + + switch (password_type) + { + case PASSWORD_TYPE_MD5: + /* + * Unfortunately we cannot perform exhaustive checks on + * encrypted passwords - we are restricted to guessing. + * (Alternatively, we could insist on the password being + * presented non-encrypted, but that has its own security + * disadvantages.) + * + * We only check for username = password. + */ + if (!pg_md5_encrypt(username, username, namelen, encrypted)) + elog(ERROR, "password encryption failed"); + if (strcmp(password, encrypted) == 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("password must not contain user name"))); + break; + + case PASSWORD_TYPE_PLAINTEXT: + /* + * For unencrypted passwords we can perform better checks + */ + + /* enforce minimum length */ + if (pwdlen < MIN_PWD_LENGTH) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("password is too short"))); + + /* check if the password contains the username */ + if (strstr(password, username)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("password must not contain user name"))); + + /* check if the password contains both letters and non-letters */ + pwd_has_letter = false; + pwd_has_nonletter = false; + for (i = 0; i < pwdlen; i++) + { + /* + * isalpha() does not work for multibyte encodings + * but let's consider non-ASCII characters non-letters + */ + if (isalpha((unsigned char) password[i])) + pwd_has_letter = true; + else + pwd_has_nonletter = true; + } + if (!pwd_has_letter || !pwd_has_nonletter) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("password must contain both letters and nonletters"))); + +#ifdef USE_CRACKLIB + /* call cracklib to check password */ + if (FascistCheck(password, CRACKLIB_DICTPATH)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("password is easily cracked"))); +#endif + break; + + default: + elog(ERROR, "unrecognized password type: %d", password_type); + break; + } + + /* all checks passed, password is ok */ +} + +/* + * Module initialization function + */ +void +_PG_init(void) +{ + /* activate password checks when the module is loaded */ + check_password_hook = check_password; +} diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml index cffbc55249..2895e6c170 100644 --- a/doc/src/sgml/contrib.sgml +++ b/doc/src/sgml/contrib.sgml @@ -1,4 +1,4 @@ - + Additional Supplied Modules @@ -98,6 +98,7 @@ psql -d dbname -f SHAREDIR/contrib/module.sql <ree; &oid2name; &pageinspect; + &passwordcheck; &pgbench; &pgbuffercache; &pgcrypto; diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml index bee66008b6..2ceee79cb9 100644 --- a/doc/src/sgml/filelist.sgml +++ b/doc/src/sgml/filelist.sgml @@ -1,4 +1,4 @@ - + @@ -111,6 +111,7 @@ + diff --git a/doc/src/sgml/passwordcheck.sgml b/doc/src/sgml/passwordcheck.sgml new file mode 100644 index 0000000000..e46e3dfa03 --- /dev/null +++ b/doc/src/sgml/passwordcheck.sgml @@ -0,0 +1,62 @@ + + + + passwordcheck + + + passwordcheck + + + + The passwordcheck module checks users' passwords + whenever they are set with + or + . + If a password is considered too weak, it will be rejected and + the command will terminate with an error. + + + + To enable this module, add '$libdir/passwordcheck' + to in + postgresql.conf, then restart the server. + + + + You can adapt this module to your needs by changing the source code. + For example, you can use + CrackLib + to check passwords — this only requires uncommenting + two lines in the Makefile and rebuilding the + module. (We cannot include CrackLib + by default for license reasons.) + Without CrackLib, the module enforces a few + simple rules for password strength, which you can modify or extend + as you see fit. + + + + + To prevent unencrypted passwords from being sent across the network, + written to the server log or otherwise stolen by a database administrator, + PostgreSQL allows the user to supply + pre-encrypted passwords. Many client programs make use of this + functionality and encrypt the password before sending it to the server. + + + This limits the usefulness of the passwordcheck + module, because in that case it can only try to guess the password. + For this reason, passwordcheck is not + recommendable if your security requirements are high. + It is more secure to use an external authentication method such as Kerberos + (see ) than to rely on + passwords within the database. + + + Alternatively, you could modify passwordcheck + to reject pre-encrypted passwords, but forcing users to set their + passwords in clear text carries its own security risks. + + + + diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c index ef546cf360..66560d7a5b 100644 --- a/src/backend/commands/user.c +++ b/src/backend/commands/user.c @@ -6,7 +6,7 @@ * Portions Copyright (c) 1996-2009, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/backend/commands/user.c,v 1.189 2009/10/07 22:14:19 alvherre Exp $ + * $PostgreSQL: pgsql/src/backend/commands/user.c,v 1.190 2009/11/18 21:57:56 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -35,8 +35,12 @@ #include "utils/tqual.h" +/* GUC parameter */ extern bool Password_encryption; +/* Hook to check passwords in CreateRole() and AlterRole() */ +check_password_hook_type check_password_hook = NULL; + static List *roleNamesToIds(List *memberNames); static void AddRoleMems(const char *rolename, Oid roleid, List *memberNames, List *memberIds, @@ -96,6 +100,8 @@ CreateRole(CreateRoleStmt *stmt) List *rolemembers = NIL; /* roles to be members of this role */ List *adminmembers = NIL; /* roles to be admins of this role */ char *validUntil = NULL; /* time the login is valid until */ + Datum validUntil_datum; /* same, as timestamptz Datum */ + bool validUntil_null; DefElem *dpassword = NULL; DefElem *dissuper = NULL; DefElem *dinherit = NULL; @@ -298,6 +304,31 @@ CreateRole(CreateRoleStmt *stmt) errmsg("role \"%s\" already exists", stmt->role))); + /* Convert validuntil to internal form */ + if (validUntil) + { + validUntil_datum = DirectFunctionCall3(timestamptz_in, + CStringGetDatum(validUntil), + ObjectIdGetDatum(InvalidOid), + Int32GetDatum(-1)); + validUntil_null = false; + } + else + { + validUntil_datum = (Datum) 0; + validUntil_null = true; + } + + /* + * Call the password checking hook if there is one defined + */ + if (check_password_hook && password) + (*check_password_hook) (stmt->role, + password, + isMD5(password) ? PASSWORD_TYPE_MD5 : PASSWORD_TYPE_PLAINTEXT, + validUntil_datum, + validUntil_null); + /* * Build a tuple to insert */ @@ -333,15 +364,8 @@ CreateRole(CreateRoleStmt *stmt) else new_record_nulls[Anum_pg_authid_rolpassword - 1] = true; - if (validUntil) - new_record[Anum_pg_authid_rolvaliduntil - 1] = - DirectFunctionCall3(timestamptz_in, - CStringGetDatum(validUntil), - ObjectIdGetDatum(InvalidOid), - Int32GetDatum(-1)); - - else - new_record_nulls[Anum_pg_authid_rolvaliduntil - 1] = true; + new_record[Anum_pg_authid_rolvaliduntil - 1] = validUntil_datum; + new_record_nulls[Anum_pg_authid_rolvaliduntil - 1] = validUntil_null; tuple = heap_form_tuple(pg_authid_dsc, new_record, new_record_nulls); @@ -419,6 +443,8 @@ AlterRole(AlterRoleStmt *stmt) int connlimit = -1; /* maximum connections allowed */ List *rolemembers = NIL; /* roles to be added/removed */ char *validUntil = NULL; /* time the login is valid until */ + Datum validUntil_datum; /* same, as timestamptz Datum */ + bool validUntil_null; DefElem *dpassword = NULL; DefElem *dissuper = NULL; DefElem *dinherit = NULL; @@ -587,6 +613,33 @@ AlterRole(AlterRoleStmt *stmt) errmsg("permission denied"))); } + /* Convert validuntil to internal form */ + if (validUntil) + { + validUntil_datum = DirectFunctionCall3(timestamptz_in, + CStringGetDatum(validUntil), + ObjectIdGetDatum(InvalidOid), + Int32GetDatum(-1)); + validUntil_null = false; + } + else + { + /* fetch existing setting in case hook needs it */ + validUntil_datum = SysCacheGetAttr(AUTHNAME, tuple, + Anum_pg_authid_rolvaliduntil, + &validUntil_null); + } + + /* + * Call the password checking hook if there is one defined + */ + if (check_password_hook && password) + (*check_password_hook) (stmt->role, + password, + isMD5(password) ? PASSWORD_TYPE_MD5 : PASSWORD_TYPE_PLAINTEXT, + validUntil_datum, + validUntil_null); + /* * Build an updated tuple, perusing the information just obtained */ @@ -666,15 +719,9 @@ AlterRole(AlterRoleStmt *stmt) } /* valid until */ - if (validUntil) - { - new_record[Anum_pg_authid_rolvaliduntil - 1] = - DirectFunctionCall3(timestamptz_in, - CStringGetDatum(validUntil), - ObjectIdGetDatum(InvalidOid), - Int32GetDatum(-1)); - new_record_repl[Anum_pg_authid_rolvaliduntil - 1] = true; - } + new_record[Anum_pg_authid_rolvaliduntil - 1] = validUntil_datum; + new_record_nulls[Anum_pg_authid_rolvaliduntil - 1] = validUntil_null; + new_record_repl[Anum_pg_authid_rolvaliduntil - 1] = true; new_tuple = heap_modify_tuple(tuple, pg_authid_dsc, new_record, new_record_nulls, new_record_repl); diff --git a/src/include/commands/user.h b/src/include/commands/user.h index 01fb92c354..ffef486b83 100644 --- a/src/include/commands/user.h +++ b/src/include/commands/user.h @@ -4,7 +4,7 @@ * Commands for manipulating roles (formerly called users). * * - * $PostgreSQL: pgsql/src/include/commands/user.h,v 1.30 2006/10/04 00:30:08 momjian Exp $ + * $PostgreSQL: pgsql/src/include/commands/user.h,v 1.31 2009/11/18 21:57:56 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -14,6 +14,14 @@ #include "nodes/parsenodes.h" +/* Hook to check passwords in CreateRole() and AlterRole() */ +#define PASSWORD_TYPE_PLAINTEXT 0 +#define PASSWORD_TYPE_MD5 1 + +typedef void (*check_password_hook_type) (const char *username, const char *password, int password_type, Datum validuntil_time, bool validuntil_null); + +extern PGDLLIMPORT check_password_hook_type check_password_hook; + extern void CreateRole(CreateRoleStmt *stmt); extern void AlterRole(AlterRoleStmt *stmt); extern void AlterRoleSet(AlterRoleSetStmt *stmt);