pam_userdb: fix password comparison timing leak

Resolves: CVE-2026-54411 and RHEL-191705
Signed-off-by: Iker Pedrosa <ipedrosa@redhat.com>
This commit is contained in:
Iker Pedrosa 2026-07-20 11:47:49 +02:00
parent 8538bf24a7
commit afa97f427c
2 changed files with 180 additions and 1 deletions

View File

@ -0,0 +1,172 @@
From 30708d973b63891bf700299ce3ae0f1086398284 Mon Sep 17 00:00:00 2001
From: vlefebvre <valentin.lefebvre@suse.com>
Date: Tue, 16 Jun 2026 16:31:29 +0200
Subject: [PATCH] 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 d79d6fdf2..86d131cf4 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>
@@ -210,4 +211,36 @@ 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;
+}
+
+/*
+ * 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 bdb8553cc..b37f096cd 100644
--- a/modules/pam_userdb/pam_userdb.c
+++ b/modules/pam_userdb/pam_userdb.c
@@ -321,15 +321,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_string(stored);
+ free(stored);
}
if (cryptmode && pam_str_skip_icase_prefix(cryptmode, "none") == NULL
@@ -361,36 +370,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 = db_firstkey(dbm);
key.dptr != NULL;
key = db_nextkey(dbm, key)) {
- 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) {
db_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_string(stored_pass);
+ free(stored_pass);
+ }
+
+ if (compare == 0) {
+ db_close(dbm);
+ return 0; /* match */
}
}
db_close(dbm);

View File

@ -4,7 +4,7 @@
Summary: An extensible library which provides authentication for applications
Name: pam
Version: 1.6.1
Release: 10%{?dist}
Release: 11%{?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 and pam_loginuid modules are GPLv2+.
@ -43,6 +43,8 @@ Patch10: pam-1.6.1-pam-faillock-skip.patch
Patch11: pam-1.6.1-pam-access-uid-gid-access-conf.patch
# https://github.com/linux-pam/linux-pam/commit/fc927d8f1a6d81e5bcf58096871684b35b793fe2
Patch12: pam-1.6.1-pam-access-fix-group-name-match.patch
# https://github.com/linux-pam/linux-pam/commit/30708d973b63891bf700299ce3ae0f1086398284
Patch13: pam-1.6.1-pam-userdb-password-timing-leak.patch
%{load:%{SOURCE3}}
@ -145,6 +147,7 @@ cp %{SOURCE18} .
%patch -P 10 -p1 -b .pam-faillock-skip
%patch -P 11 -p1 -b .pam-access-uid-gid-access-conf
%patch -P 12 -p1 -b .pam-access-fix-group-name-match
%patch -P 13 -p1 -b .pam-userdb-password-timing-leak
autoreconf -i
@ -383,6 +386,10 @@ done
%{_pam_libdir}/libpam_misc.so.%{so_ver}*
%changelog
* Mon Jul 20 2026 Iker Pedrosa <ipedrosa@redhat.com> - 1.6.1-11
- pam_userdb: fix password comparison timing leak.
Resolves: CVE-2026-54411 and RHEL-191705
* Tue Apr 7 2026 Iker Pedrosa <ipedrosa@redhat.com> - 1.6.1-10
- pam_access: support UID and GID in access.conf
Resolves: RHEL-119867