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:
parent
4d79519f12
commit
8fef4ceb64
196
pam-1.5.1-pam-userdb-password-timing-leak.patch
Normal file
196
pam-1.5.1-pam-userdb-password-timing-leak.patch
Normal 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);
|
||||
10
pam.spec
10
pam.spec
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user