pam_userdb: fix password comparison timing leak

Resolves: CVE-2026-54411 and RHEL-191702
Signed-off-by: Iker Pedrosa <ipedrosa@redhat.com>
This commit is contained in:
Iker Pedrosa 2026-07-20 12:34:00 +02:00
parent 4d79519f12
commit 8fef4ceb64
2 changed files with 205 additions and 1 deletions

View File

@ -0,0 +1,196 @@
From 3dd6b2b290ab295125c5a5191dd387472d09a800 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Christian=20G=C3=B6ttsche?= <cgzones@googlemail.com>
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 666a0285..f0ff2533 100644
--- a/libpam/include/pam_inline.h
+++ b/libpam/include/pam_inline.h
@@ -152,4 +152,18 @@ pam_read_passwords(int fd, int npass, char **passwords)
return i;
}
+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 874ddf410bf6c1e679c155422858f80e81d05a55 Mon Sep 17 00:00:00 2001
From: vlefebvre <valentin.lefebvre@suse.com>
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 <ctype.h>.
(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 <ldv@strace.io>
---
libpam/include/pam_inline.h | 20 +++++++++
modules/pam_userdb/pam_userdb.c | 75 +++++++++++++++++++--------------
2 files changed, 64 insertions(+), 31 deletions(-)
diff --git a/libpam/include/pam_inline.h b/libpam/include/pam_inline.h
index f0ff2533..10d0da2a 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 <ctype.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
@@ -166,4 +167,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 dc2ca232..1c979af5 100644
--- a/modules/pam_userdb/pam_userdb.c
+++ b/modules/pam_userdb/pam_userdb.c
@@ -242,15 +242,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 && pam_str_skip_icase_prefix(cryptmode, "none") == NULL
@@ -282,36 +291,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);

View File

@ -3,7 +3,7 @@
Summary: An extensible library which provides authentication for applications
Name: pam
Version: 1.5.1
Release: 29%{?dist}
Release: 30%{?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+.
@ -90,6 +90,9 @@ Patch28: pam-1.5.1-pam-faillock-skip.patch
Patch29: pam-1.5.1-pam-access-uid-gid-access-conf.patch
# https://github.com/linux-pam/linux-pam/commit/2e7910e3be93d99e865d9b86c38a1b56e4a95d6e
Patch30: pam-1.5.1-pam-lastlog-file-locking.patch
# https://github.com/linux-pam/linux-pam/commit/c11ccdfad1596199713f75a61f34672f7529ab73
# https://github.com/linux-pam/linux-pam/commit/30708d973b63891bf700299ce3ae0f1086398284
Patch31: pam-1.5.1-pam-userdb-password-timing-leak.patch
%global _pamlibdir %{_libdir}
%global _moduledir %{_libdir}/security
@ -202,6 +205,7 @@ cp %{SOURCE18} .
%patch28 -p1 -b .pam-faillock-skip
%patch29 -p1 -b .pam-access-uid-gid-access-con
%patch30 -p1 -b .pam-lastlog-file-locking
%patch31 -p1 -b .pam-userdb-password-timing-leak
autoreconf -i
@ -457,6 +461,10 @@ done
%doc doc/sag/*.txt doc/sag/html
%changelog
* Mon Jul 20 2026 Iker Pedrosa <ipedrosa@redhat.com> - 1.5.1-30
- pam_userdb: fix password comparison timing leak.
Resolves: CVE-2026-54411 and RHEL-191702
* Thu Apr 9 2026 Iker Pedrosa <ipedrosa@redhat.com> - 1.5.1-29
- pam_access: support UID and GID in access.conf
Resolves: RHEL-119868