diff --git a/0001-pam-fix-use-after-free-during-p11_child-processing.patch b/0001-pam-fix-use-after-free-during-p11_child-processing.patch new file mode 100644 index 0000000..90a009b --- /dev/null +++ b/0001-pam-fix-use-after-free-during-p11_child-processing.patch @@ -0,0 +1,111 @@ +From f2c69b916f5fe53a930aa39c2078b248b83bc2b4 Mon Sep 17 00:00:00 2001 +From: Alexey Tikhonov +Date: Mon, 29 Jun 2026 21:46:46 +0200 +Subject: [PATCH] PAM: fix use-after-free during p11_child processing + +`pam_check_cert_send()` stored `pctx->sss_certmap_ctx` in the +request state. If `p11_refresh_certmap_ctx()` ran while `p11_child` +was still executing (e.g. triggered by a domain refresh), it freed +and replaced the certmap context, leaving the request state holding +a dangling pointer. `p11_child_done()` could later use that pointer. + +Fix this by passing the `pam_ctx` into `pam_check_cert_send()` and +dereferencing `pctx->sss_certmap_ctx` at the time it is actually needed +in `p11_child_done()`, so the current context is always used. + +Resolves: https://github.com/SSSD/sssd/issues/8796 +Fixes: CVE-2026-12610 + +Assisted-By: Claude Code (Opus 4.6) +Reviewed-by: Iker Pedrosa +Reviewed-by: Sumit Bose +(cherry picked from commit fa7a55949a30fed064a28ea6f0c801fc5e8c5ba7) +--- + src/responder/pam/pamsrv.h | 2 +- + src/responder/pam/pamsrv_cmd.c | 2 +- + src/responder/pam/pamsrv_p11.c | 11 ++++++----- + 3 files changed, 8 insertions(+), 7 deletions(-) + +diff --git a/src/responder/pam/pamsrv.h b/src/responder/pam/pamsrv.h +index 694b391bb..162bfe1a4 100644 +--- a/src/responder/pam/pamsrv.h ++++ b/src/responder/pam/pamsrv.h +@@ -153,7 +153,7 @@ struct tevent_req *pam_check_cert_send(TALLOC_CTX *mem_ctx, + const char *ca_db, + time_t timeout, + const char *verify_opts, +- struct sss_certmap_ctx *sss_certmap_ctx, ++ struct pam_ctx *pctx, + const char *uri, + struct pam_data *pd); + errno_t pam_check_cert_recv(struct tevent_req *req, TALLOC_CTX *mem_ctx, +diff --git a/src/responder/pam/pamsrv_cmd.c b/src/responder/pam/pamsrv_cmd.c +index cd003ff46..7fadf647d 100644 +--- a/src/responder/pam/pamsrv_cmd.c ++++ b/src/responder/pam/pamsrv_cmd.c +@@ -1945,7 +1945,7 @@ static errno_t check_cert(TALLOC_CTX *mctx, + + req = pam_check_cert_send(mctx, ev, + pctx->ca_db, p11_child_timeout, +- cert_verification_opts, pctx->sss_certmap_ctx, ++ cert_verification_opts, pctx, + uri, pd); + if (req == NULL) { + DEBUG(SSSDBG_OP_FAILURE, "pam_check_cert_send failed.\n"); +diff --git a/src/responder/pam/pamsrv_p11.c b/src/responder/pam/pamsrv_p11.c +index 191bf40c5..29a6edc05 100644 +--- a/src/responder/pam/pamsrv_p11.c ++++ b/src/responder/pam/pamsrv_p11.c +@@ -749,7 +749,7 @@ done: + + struct pam_check_cert_state { + struct tevent_context *ev; +- struct sss_certmap_ctx *sss_certmap_ctx; ++ struct pam_ctx *pctx; + struct child_io_fds *io; + struct cert_auth_info *cert_list; + struct pam_data *pam_data; +@@ -763,7 +763,7 @@ struct tevent_req *pam_check_cert_send(TALLOC_CTX *mem_ctx, + const char *ca_db, + time_t timeout, + const char *verify_opts, +- struct sss_certmap_ctx *sss_certmap_ctx, ++ struct pam_ctx *pctx, + const char *uri, + struct pam_data *pd) + { +@@ -791,11 +791,12 @@ struct tevent_req *pam_check_cert_send(TALLOC_CTX *mem_ctx, + goto done; + } + +- if (sss_certmap_ctx == NULL) { ++ if (pctx == NULL || pctx->sss_certmap_ctx == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "Missing certificate matching context.\n"); + ret = EINVAL; + goto done; + } ++ state->pctx = pctx; + + state->pam_data = pd; + +@@ -880,7 +881,6 @@ struct tevent_req *pam_check_cert_send(TALLOC_CTX *mem_ctx, + } + + state->ev = ev; +- state->sss_certmap_ctx = sss_certmap_ctx; + + ret = sss_child_start(state, ev, + P11_CHILD_PATH, extra_args, false, +@@ -985,7 +985,8 @@ static void p11_child_done(struct tevent_req *subreq) + + FD_CLOSE(state->io->read_from_child_fd); + +- ret = parse_p11_child_response(state, buf, buf_len, state->sss_certmap_ctx, ++ ret = parse_p11_child_response(state, buf, buf_len, ++ state->pctx->sss_certmap_ctx, + &state->cert_list); + if (ret != EOK) { + if (ret == ERR_P11_PIN_LOCKED) { +-- +2.54.0 + diff --git a/0002-oidc_child-change-default-with-no-auth-method.patch b/0002-oidc_child-change-default-with-no-auth-method.patch new file mode 100644 index 0000000..17a0ae7 --- /dev/null +++ b/0002-oidc_child-change-default-with-no-auth-method.patch @@ -0,0 +1,54 @@ +From 9884f33477b3e280f37c41465eb28260fea8522c Mon Sep 17 00:00:00 2001 +From: Sumit Bose +Date: Mon, 15 Jun 2026 17:42:29 +0200 +Subject: [PATCH] oidc_child: change default with no auth method +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Before the new client-auth-method option was added oidc_child was +accepting a client or nothing in case the IdP client is a public one. +With the introduction of the client-auth-method option the default if +not option was given was set the 'secret' which cause a regression for +public clients. + +With this patch the old default behavior, accepting a secret or nothing, +is restored. + +Reviewed-by: Alexey Tikhonov +Reviewed-by: Pavel Březina +(cherry picked from commit 10bad8b1d74b53a90e10f0a7a74565d4272a01bf) +--- + src/oidc_child/oidc_child.c | 9 +++++++-- + 1 file changed, 7 insertions(+), 2 deletions(-) + +diff --git a/src/oidc_child/oidc_child.c b/src/oidc_child/oidc_child.c +index 8aa7f7ea4..15e88d4da 100644 +--- a/src/oidc_child/oidc_child.c ++++ b/src/oidc_child/oidc_child.c +@@ -410,7 +410,11 @@ static bool set_client_auth_method(const char *tmp_cam, struct cli_opts *opts) + return false; + } + } else { +- opts->client_auth_method = CAM_SECRET; ++ if (has_creds_client_secret(opts)) { ++ opts->client_auth_method = CAM_SECRET; ++ } else { ++ opts->client_auth_method = CAM_NONE; ++ } + } + + switch (opts->client_auth_method) { +@@ -502,7 +506,8 @@ static int parse_cli(int argc, const char *argv[], struct cli_opts *opts) + {"pkcs12-client-creds", 0, POPT_ARG_STRING, &opts->pkcs12_client_creds, 0, + _("Client certificate and key in PKCS#12 format"), NULL}, + {"client-auth-method", 0, POPT_ARG_STRING, &tmp_cam, 0, +- _("Authentication method against IdP [secret (default), mtls, jwt, none]"), ++ _("Authentication method against IdP [secret, mtls, jwt, none] " ++ "(default: secret if client secret given, none otherwise)"), + NULL}, + {"ca-db", 0, POPT_ARG_STRING, &opts->ca_db, 0, + _("Path to PEM file with CA certificates"), NULL}, +-- +2.54.0 + diff --git a/0003-cfg_rules-allow-ldap_sasl_authid-in-subdomain.patch b/0003-cfg_rules-allow-ldap_sasl_authid-in-subdomain.patch new file mode 100644 index 0000000..92baa46 --- /dev/null +++ b/0003-cfg_rules-allow-ldap_sasl_authid-in-subdomain.patch @@ -0,0 +1,37 @@ +From f0a3dbe607079d3501061dc8b60822a478dddfa3 Mon Sep 17 00:00:00 2001 +From: Alexey Tikhonov +Date: Tue, 30 Jun 2026 20:30:24 +0200 +Subject: [PATCH] cfg_rules: allow + ldap_sasl_authid/ldap_krb5_keytab/krb5_keytab +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +in subdomain section. + +Note that there is a bug: those values are overwritten by domain's +`krb5_keytab` option. But "by design" they are supposed to work. + +Reviewed-by: Tomáš Halman +(cherry picked from commit 27651161b625b9c2d748686893302b19df59750d) +--- + src/config/cfg_rules.ini | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/src/config/cfg_rules.ini b/src/config/cfg_rules.ini +index 4ef0d0156..3d79f4374 100644 +--- a/src/config/cfg_rules.ini ++++ b/src/config/cfg_rules.ini +@@ -831,6 +831,9 @@ option = ldap_group_search_base + option = ldap_netgroup_search_base + option = ldap_service_search_base + option = ldap_sasl_mech ++option = ldap_sasl_authid ++option = ldap_krb5_keytab ++option = krb5_keytab + option = ad_server + option = ad_backup_server + option = ad_site +-- +2.54.0 + diff --git a/0004-gpo-reject-path-traversal-in-gPCFileSysPath.patch b/0004-gpo-reject-path-traversal-in-gPCFileSysPath.patch new file mode 100644 index 0000000..776dfb7 --- /dev/null +++ b/0004-gpo-reject-path-traversal-in-gPCFileSysPath.patch @@ -0,0 +1,176 @@ +From 25bacdb4ad096bb753192da776c1b7bb9900710c Mon Sep 17 00:00:00 2001 +From: Alexey Tikhonov +Date: Thu, 2 Jul 2026 17:29:51 +0200 +Subject: [PATCH] gpo: reject path traversal in gPCFileSysPath +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The gPCFileSysPath LDAP attribute from AD Group Policy Objects is parsed +by ad_gpo_extract_smb_components() which converts backslashes to forward +slashes but does not reject ".." path traversal sequences. The resulting +smb_path is used directly in gpo_cache_store_file() to construct a local +filesystem path under GPO_CACHE_PATH, allowing an attacker with GPO +write access to write files outside the cache directory. + +Due to differential path resolution between libsmbclient (which clamps +".." at the SMB share root) and the kernel (which resolves ".." fully), +the SMB download succeeds while the local file write escapes the cache. +On systems with SELinux enforcing, this enables Kerberos configuration +injection via /var/lib/sss/pubconf/krb5.include.d/ (sssd_public_t, +writable by sssd_t). On systems without SELinux, this enables arbitrary +file writes including cron job injection for root code execution. + +This patch adds two layers of defense: + +1. Reject ".." as a path component in smb_path at parse time in + ad_gpo_extract_smb_components(). Uses component-aware validation + that checks for "/..", "../", and exact ".." — not substring matching + which would false-positive on legitimate names containing "..". + +2. Validate the resolved cache path stays within GPO_CACHE_PATH in + gpo_cache_store_file() using realpath(), with a trailing-slash + prefix check to prevent prefix-collision attacks (e.g., + /var/lib/sss/gpo_cache_evil/ matching /var/lib/sss/gpo_cache). + +Based on the patch by: Ian Murphy +Amended by: Alexey Tikhonov + +:fixes: CVE-2026-14476 + +Reviewed-by: Sumit Bose +Reviewed-by: Tomáš Halman +(cherry picked from commit ba207eab76ff5253662a763b9b6e9ea42f03d31b) +--- + src/providers/ad/ad_gpo.c | 47 +++++++++++++++++++++++++++++++ + src/providers/ad/ad_gpo_child.c | 49 +++++++++++++++++++++++++++++++++ + 2 files changed, 96 insertions(+) + +diff --git a/src/providers/ad/ad_gpo.c b/src/providers/ad/ad_gpo.c +index 5c2f117a5..b8562edc5 100644 +--- a/src/providers/ad/ad_gpo.c ++++ b/src/providers/ad/ad_gpo.c +@@ -3832,6 +3832,43 @@ ad_gpo_populate_candidate_gpos(TALLOC_CTX *mem_ctx, + return ret; + } + ++/* ++ * Check whether a path contains ".." as a path component. ++ * Returns true if traversal is detected, false if the path is safe. ++ * ++ * Checks for: ++ * - "/.." anywhere in the path (component starting with ..) ++ * - "../" at the start of the path ++ * - exact match ".." (path is just "..") ++ * - "/.." at the end of the path ++ * ++ * Does NOT match ".." as a substring of a longer component ++ * (e.g., "my..file" is allowed). ++ */ ++static bool gpo_path_has_traversal(const char *path) ++{ ++ const char *p; ++ ++ if (path == NULL) { ++ return false; ++ } ++ ++ /* Exact match */ ++ if (strcmp(path, "..") == 0) return true; ++ ++ /* Starts with ../ */ ++ if (strncmp(path, "../", 3) == 0) return true; ++ ++ /* Contains /../ or ends with /.. */ ++ p = path; ++ while ((p = strstr(p, "/..")) != NULL) { ++ if (p[3] == '/' || p[3] == '\0') return true; ++ p += 3; ++ } ++ ++ return false; ++} ++ + /* + * This function parses the input_path into its components, replaces each + * back slash ('\') with a forward slash ('/'), and populates the output params. +@@ -3908,6 +3945,16 @@ ad_gpo_extract_smb_components(TALLOC_CTX *mem_ctx, + goto done; + } + ++ /* Reject path traversal. See function comment for what is matched. */ ++ if (gpo_path_has_traversal(smb_path)) { ++ DEBUG(SSSDBG_CRIT_FAILURE, ++ "gPCFileSysPath contains path traversal component '..': " ++ "[%s]. Rejecting to prevent cache directory escape.\n", ++ smb_path); ++ ret = EINVAL; ++ goto done; ++ } ++ + *_smb_server = talloc_asprintf(mem_ctx, "%s%s", + SMB_STANDARD_URI, + server_hostname); +diff --git a/src/providers/ad/ad_gpo_child.c b/src/providers/ad/ad_gpo_child.c +index b8b47b09e..db3716fdf 100644 +--- a/src/providers/ad/ad_gpo_child.c ++++ b/src/providers/ad/ad_gpo_child.c +@@ -322,6 +322,55 @@ static errno_t gpo_cache_store_file(const char *smb_path, + goto done; + } + ++ /* Defense-in-depth: verify the resolved path stays within the cache ++ * directory (when updating existing files). This catches any bypass ++ * of the ".." check in the parser, including encoding tricks, symlink ++ * attacks, or future regressions. ++ * ++ * The trailing-slash comparison prevents prefix-collision attacks: ++ * without it, a path resolving to "/var/lib/sss/gpo_cache_evil/" ++ * would incorrectly match the prefix "/var/lib/sss/gpo_cache". ++ */ ++ { ++ char *resolved = realpath(filename, NULL); ++ if (resolved != NULL) { ++ /* Resolve GPO_CACHE_PATH too so the comparison works ++ * even when the cache path contains symlinks. */ ++ char *resolved_cache = realpath(GPO_CACHE_PATH, NULL); ++ if (resolved_cache == NULL) { ++ ret = errno; ++ DEBUG(SSSDBG_CRIT_FAILURE, ++ "realpath(\"%s\") failed: [%d][%s]\n", ++ GPO_CACHE_PATH, ret, strerror(ret)); ++ free(resolved); ++ goto done; ++ } ++ ++ /* Check that resolved path starts with resolved cache + "/" */ ++ size_t cache_len = strlen(resolved_cache); ++ bool inside = ((strlen(resolved) >= cache_len) && ++ (strncmp(resolved, resolved_cache, cache_len) == 0) && ++ (resolved[cache_len] == '/' || resolved[cache_len] == '\0')); ++ if (!inside) { ++ DEBUG(SSSDBG_CRIT_FAILURE, ++ "GPO cache path escapes cache directory: [%s] " ++ "resolves to [%s] which is outside [%s]. " ++ "Rejecting.\n", ++ filename, resolved, resolved_cache); ++ free(resolved_cache); ++ free(resolved); ++ ret = EINVAL; ++ goto done; ++ } ++ free(resolved_cache); ++ free(resolved); ++ } ++ /* If realpath returns NULL, the path doesn't exist yet. ++ * prepare_gpo_cache() will create it — the mkdir calls ++ * are validated by SELinux MAC policy. ++ */ ++ } ++ + tmp_name = talloc_asprintf(tmp_ctx, "%sXXXXXX", filename); + if (tmp_name == NULL) { + DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf failed.\n"); +-- +2.54.0 + diff --git a/0005-sudo-warn-when-ldap_sudo_search_base-falls-back-to-root-DN.patch b/0005-sudo-warn-when-ldap_sudo_search_base-falls-back-to-root-DN.patch new file mode 100644 index 0000000..2573db8 --- /dev/null +++ b/0005-sudo-warn-when-ldap_sudo_search_base-falls-back-to-root-DN.patch @@ -0,0 +1,81 @@ +From c29afbf02acf9e235eb5283f0325ea0037c1574a Mon Sep 17 00:00:00 2001 +From: Alexey Tikhonov +Date: Fri, 3 Jul 2026 13:25:08 +0200 +Subject: [PATCH] sudo: warn when ldap_sudo_search_base falls back to root DN +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +When ldap_sudo_search_base is not explicitly configured, SSSD falls back +to the domain's naming context (root DN) and searches the entire LDAP +directory tree for sudoRole objects. Any LDAP principal with write access +to any subtree can inject a sudoRole granting arbitrary sudo privileges +on every enrolled host. + +This patch adds a warning log when the fallback occurs, alerting +administrators that their configuration searches the entire directory +tree for sudo rules. A future hardening step would be to default to +ou=sudoers, instead of the root DN. + +The warning approach preserves backwards compatibility while ensuring +administrators are aware of the security implications. + +Based on the patch by: Ian Murphy +Amended by: Alexey Tikhonov + +:fixes: CVE-2026-14474 + +Reviewed-by: Sumit Bose +Reviewed-by: Tomáš Halman +(cherry picked from commit ff8c1b19bcdbf79b733b052a7d926bd920b1205d) +--- + src/providers/ldap/sdap.c | 19 +++++++++++++++++++ + src/tests/system/tests/test_ldap.py | 2 +- + 2 files changed, 20 insertions(+), 1 deletion(-) + +diff --git a/src/providers/ldap/sdap.c b/src/providers/ldap/sdap.c +index 1ce1d704b..7aa9244aa 100644 +--- a/src/providers/ldap/sdap.c ++++ b/src/providers/ldap/sdap.c +@@ -1341,6 +1341,25 @@ errno_t sdap_set_config_options_with_rootdse(struct sysdb_attrs *rootdse, + + /* Sudo */ + if (!sdom->sudo_search_bases) { ++ /* At some point make this option mandatory, ++ * i.e. disable sudo rules lookup if 'sudo_search_bases' not set. ++ */ ++ DEBUG(SSSDBG_IMPORTANT_INFO, ++ "`ldap_sudo_search_base` is not set. SSSD will search the entire " ++ "directory tree (%s) for sudoRole objects. This may allow any " ++ "LDAP principal with write access to any subtree to inject " ++ "sudo rules granting arbitrary privileges. Set " ++ "`ldap_sudo_search_base` to restrict the search scope " ++ "(e.g., 'ou=sudoers,dc=example,dc=com').\n", ++ sdom->naming_context); ++ sss_log(SSS_LOG_ALERT, ++ "`ldap_sudo_search_base` is not set. SSSD will search the entire " ++ "directory tree (%s) for sudoRole objects. This may allow any " ++ "LDAP principal with write access to any subtree to inject " ++ "sudo rules granting arbitrary privileges. Set " ++ "`ldap_sudo_search_base` to restrict the search scope " ++ "(e.g., 'ou=sudoers,dc=example,dc=com').", ++ sdom->naming_context); + ret = sdap_set_search_base(opts, sdom, + SDAP_SUDO_SEARCH_BASE, + sdom->naming_context); +diff --git a/src/tests/system/tests/test_ldap.py b/src/tests/system/tests/test_ldap.py +index 9e21d73f6..36d988475 100644 +--- a/src/tests/system/tests/test_ldap.py ++++ b/src/tests/system/tests/test_ldap.py +@@ -245,7 +245,7 @@ def test_ldap__search_base_is_discovered_and_defaults_to_root_dse(client: Client + client.sssd.dom("test")["ldap_search_base"] = ldap.ldap.naming_context + + client.sssd.stop() +- client.sssd.clear() ++ client.sssd.clear(logs=True) + client.sssd.start() + + assert client.auth.ssh.password("puser1", "Secret123"), "User 'puser1' login failed!" +-- +2.54.0 + diff --git a/sssd.spec b/sssd.spec index cf9f562..f527441 100644 --- a/sssd.spec +++ b/sssd.spec @@ -21,7 +21,7 @@ Name: sssd Version: 2.13.1 -Release: 1%{?dist} +Release: 2%{?dist} Summary: System Security Services Daemon License: GPL-3.0-or-later URL: https://github.com/SSSD/sssd/ @@ -29,6 +29,11 @@ Source0: https://github.com/SSSD/sssd/releases/download/%{version}/sssd-%{versio Source1: sssd.sysusers ### Patches ### +Patch1: 0001-pam-fix-use-after-free-during-p11_child-processing.patch +Patch2: 0002-oidc_child-change-default-with-no-auth-method.patch +Patch3: 0003-cfg_rules-allow-ldap_sasl_authid-in-subdomain.patch +Patch4: 0004-gpo-reject-path-traversal-in-gPCFileSysPath.patch +Patch5: 0005-sudo-warn-when-ldap_sudo_search_base-falls-back-to-root-DN.patch ### Dependencies ### @@ -1087,6 +1092,13 @@ fi %systemd_postun_with_restart sssd.service %changelog +* Thu Jul 2 2026 Madhuri Upadhye - 2.13.1-2 +- Resolves: RHEL-190529 CVE-2026-12610 sssd: Use-after-free crash in SSSD' 'sssd_pam' process +- Resolves: RHEL-185008 Regression in IPA + Idp authentication with keycloak +- Resolves: RHEL-111571 'sssctl config-check' does not work correctly for "Accessing AD with a MSA" +- Resolves: RHEL-192054 CVE-2026-14474 sssd: sudo ldap provider searches entire directory tree for sudorole objects by default, enabling privilege escalation +- Resolves: RHEL-192069 CVE-2026-14476 sssd: path traversal in SSSD AD GPO provider allows writing files outside GPO cache directory as root + * Wed Jun 10 2026 Sumit Bose - 2.13.1-1 - Resolves: RHEL-183331 Rebase SSSD for RHEL 10.3 - Resolves: RHEL-178180 'sssctl analyze --help' shows incorrect command in usage field