diff --git a/pam-1.3.1-CVE-2026-54411.patch b/pam-1.3.1-CVE-2026-54411.patch new file mode 100644 index 0000000..5dd4ef5 --- /dev/null +++ b/pam-1.3.1-CVE-2026-54411.patch @@ -0,0 +1,204 @@ +From 2cde1639a00d9c46ed5677a65aa3ef4e7ed4fede Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Christian=20G=C3=B6ttsche?= +Date: Sat, 20 Jan 2024 14:03:51 +0100 +Subject: [PATCH 1/2] libpam: add helper to compare strings in constant time + +Add a helper function to compare two strings for equality, that performs +the same amount of operations based on the first argument, regardless of +the length of the second argument, or the position of the first +difference. +This can be used as defense-in-depth mitigation against timing attacks +of password comparisons. +--- + libpam/include/pam_inline.h | 14 ++++++++++++++ + 1 file changed, 14 insertions(+) + +diff --git a/libpam/include/pam_inline.h b/libpam/include/pam_inline.h +index dd93394..2450d50 100644 +--- a/libpam/include/pam_inline.h ++++ b/libpam/include/pam_inline.h +@@ -100,4 +100,18 @@ pam_snprintf(char *str, size_t size, const char *fmt, ...) + pam_snprintf((str_), sizeof(str_) + PAM_MUST_BE_ARRAY(str_), (fmt_), \ + ##__VA_ARGS__) + ++static inline int ++pam_consttime_streq(const char *userinput, const char *secret) { ++ volatile const char *u = userinput, *s = secret; ++ volatile int ret = 0; ++ ++ do { ++ ret |= *u ^ *s; ++ ++ s += !!*s; ++ } while (*u++ != '\0'); ++ ++ return ret == 0; ++} ++ + #endif /* PAM_INLINE_H */ + +From f22f663d1fdf3ab53a9f569190a0070a130dd7d5 Mon Sep 17 00:00:00 2001 +From: vlefebvre +Date: Tue, 16 Jun 2026 16:31:29 +0200 +Subject: [PATCH 2/2] pam_userdb: fix password comparison timing leak + +* libpam/include/pam_inline.h: Include . +(pam_consttime_strcaseeq): New function that implements a constant-time, +case-insensitive string equality check. +* modules/pam_userdb/pam_userdb.c (user_lookup): Use it along with +pam_consttime_streq instead of strncmp and strncasecmp to fix +timing side-channel that leaks password prefix bytes and length +(CWE-208). + +Resolves: https://github.com/linux-pam/linux-pam/issues/992 +Co-authored-by: Dmitry V. Levin +--- + libpam/include/pam_inline.h | 20 +++++++++ + modules/pam_userdb/pam_userdb.c | 76 +++++++++++++++++++-------------- + 2 files changed, 65 insertions(+), 31 deletions(-) + +diff --git a/libpam/include/pam_inline.h b/libpam/include/pam_inline.h +index 2450d50..b6aa0cc 100644 +--- a/libpam/include/pam_inline.h ++++ b/libpam/include/pam_inline.h +@@ -9,6 +9,7 @@ + #define PAM_INLINE_H + + #include "pam_cc_compat.h" ++#include + #include + #include + #include +@@ -114,4 +115,23 @@ pam_consttime_streq(const char *userinput, const char *secret) { + return ret == 0; + } + ++/* ++ * Constant-time, case-insensitive string equality check. ++ * Same contract as pam_consttime_streq but uses tolower() on each byte. ++ * Runs for exactly strlen(userinput)+1 iterations regardless of secret. ++ */ ++static inline int ++pam_consttime_strcaseeq(const char *userinput, const char *secret) { ++ volatile const char *u = userinput, *s = secret; ++ volatile int ret = 0; ++ ++ do { ++ ret |= tolower((unsigned char)*u) ^ tolower((unsigned char)*s); ++ ++ s += !!*s; ++ } while (*u++ != '\0'); ++ ++ return ret == 0; ++} ++ + #endif /* PAM_INLINE_H */ +diff --git a/modules/pam_userdb/pam_userdb.c b/modules/pam_userdb/pam_userdb.c +index 8794cf6..7d425c3 100644 +--- a/modules/pam_userdb/pam_userdb.c ++++ b/modules/pam_userdb/pam_userdb.c +@@ -48,6 +48,7 @@ + #include + #include + #include ++#include "pam_inline.h" + + /* + * Conversation function to obtain the user's password +@@ -259,15 +260,24 @@ user_lookup (pam_handle_t *pamh, const char *database, const char *cryptmode, + } else { + + /* Unknown password encryption method - +- * default to plaintext password storage ++ * default to plaintext password storage. ++ * Use constant-time comparison: strncmp/strncasecmp leak prefix bytes ++ * and the length pre-check leaks the password length (CWE-208). + */ + +- if (strlen(pass) != (size_t)data.dsize) { +- compare = 1; /* wrong password len -> wrong password */ +- } else if (ctrl & PAM_ICASE_ARG) { +- compare = strncasecmp(data.dptr, pass, data.dsize); ++ /* libdb is not guaranteed to produce null-terminated strings */ ++ char *stored = strndup(data.dptr, data.dsize); ++ if (stored == NULL) { ++ pam_syslog(pamh, LOG_CRIT, "strndup failed: data.dptr"); ++ compare = -2; + } else { +- compare = strncmp(data.dptr, pass, data.dsize); ++ if (ctrl & PAM_ICASE_ARG) { ++ compare = pam_consttime_strcaseeq(pass, stored) ? 0 : 1; ++ } else { ++ compare = pam_consttime_streq(pass, stored) ? 0 : 1; ++ } ++ _pam_overwrite(stored); ++ free(stored); + } + + if (cryptmode && strncasecmp(cryptmode, "none", 4) +@@ -299,36 +309,40 @@ user_lookup (pam_handle_t *pamh, const char *database, const char *cryptmode, + } + + /* now handle the key_only case */ ++ size_t ulen = strlen(user); + for (key = dbm_firstkey(dbm); + key.dptr != NULL; + key = dbm_nextkey(dbm)) { +- int compare; +- /* first compare the user portion (case sensitive) */ +- compare = strncmp(key.dptr, user, strlen(user)); +- if (compare == 0) { +- /* assume failure */ +- compare = -1; +- /* if we have the divider where we expect it to be... */ +- if (key.dptr[strlen(user)] == '-') { +- saw_user = 1; +- if ((size_t)key.dsize == strlen(user) + 1 + strlen(pass)) { +- if (ctrl & PAM_ICASE_ARG) { +- /* compare the password portion (case insensitive)*/ +- compare = strncasecmp(key.dptr + strlen(user) + 1, +- pass, +- strlen(pass)); +- } else { +- /* compare the password portion (case sensitive) */ +- compare = strncmp(key.dptr + strlen(user) + 1, +- pass, +- strlen(pass)); +- } +- } +- } +- if (compare == 0) { ++ /* assume failure */ ++ int compare = -1; ++ ++ /* ++ * First compare the user portion (case sensitive); ++ * user is caller-supplied, so this memcmp leaks nothing secret. ++ */ ++ if ((size_t)key.dsize > ulen && ++ key.dptr[ulen] == '-' && ++ memcmp(key.dptr, user, ulen) == 0) { ++ saw_user = 1; ++ char *stored_pass = strndup(key.dptr + ulen + 1, ++ key.dsize - ulen - 1); ++ if (stored_pass == NULL) { + dbm_close(dbm); +- return 0; /* match */ ++ return -2; + } ++ /* compare the password portion (case (in)sensitive) */ ++ if (ctrl & PAM_ICASE_ARG) { ++ compare = pam_consttime_strcaseeq(pass, stored_pass) ? 0 : 1; ++ } else { ++ compare = pam_consttime_streq(pass, stored_pass) ? 0 : 1; ++ } ++ _pam_overwrite(stored_pass); ++ free(stored_pass); ++ } ++ ++ if (compare == 0) { ++ dbm_close(dbm); ++ return 0; /* match */ + } + } + dbm_close(dbm); diff --git a/pam.spec b/pam.spec index 06e1d20..d948743 100644 --- a/pam.spec +++ b/pam.spec @@ -3,7 +3,7 @@ Summary: An extensible library which provides authentication for applications Name: pam Version: 1.3.1 -Release: 39%{?dist} +Release: 40%{?dist} # The library is BSD licensed with option to relicense as GPLv2+ # - this option is redundant as the BSD license allows that anyway. # pam_timestamp, pam_loginuid, and pam_console modules are GPLv2+. @@ -131,6 +131,9 @@ Patch77: pam-1.3.1-pam-namespace-rebase.patch # https://github.com/linux-pam/linux-pam/commit/51a06bc8cc2278c6e81c9c08a9381c9eb0d2de96 # https://github.com/linux-pam/linux-pam/commit/470b5bdd8fd29d6b35e3a80f9a57bdd4b2438200 Patch78: pam-1.3.1-pam-unix-blank-expiration.patch +# https://github.com/linux-pam/linux-pam/commit/c11ccdfad1596199713f75a61f34672f7529ab73 +# https://github.com/linux-pam/linux-pam/commit/30708d973b63891bf700299ce3ae0f1086398284 +Patch79: pam-1.3.1-CVE-2026-54411.patch %define _pamlibdir %{_libdir} %define _moduledir %{_libdir}/security @@ -257,6 +260,7 @@ cp %{SOURCE18} . %patch76 -p1 -b .pam-inline-pam-asprintf %patch77 -p1 -b .pam-namespace-rebase %patch78 -p1 -b .pam-unix-blank-expiration +%patch79 -p1 -b .CVE-2026-54411 autoreconf -i @@ -510,6 +514,10 @@ done %doc doc/specs/rfc86.0.txt %changelog +* Sun Jul 12 2026 RHEL Packaging Agent - 1.3.1-40 +- pam_userdb: fix password comparison timing leak. + Resolves: CVE-2026-54411 and RHEL-191699 + * Fri Nov 14 2025 Iker Pedrosa - 1.3.1-39 - pam_unix: sync expiry checks with shadow. Resolves: RHEL-70476