From 1c5146ddfc736e9d790bd91f3124c6fba6847bb3 Mon Sep 17 00:00:00 2001
From: Stefan Metzmacher <metze@samba.org>
Date: Wed, 18 Mar 2026 12:24:47 +0100
Subject: [PATCH] CVE-2026-4408: s3:samr-server: deny, mask and/or single quote
 username to 'check password script'

We pass this on to the check password script, prevent remote command
execution.

We now try to autodetect if we could implicitly use '%u' for the
replacement and fallback to a fixed fallback username.

Admins should make use of SAMBA_CPS_ACCOUNT_NAME
instead of passing '%u' to 'check password script'

BUG: https://bugzilla.samba.org/show_bug.cgi?id=16034

Pair-Programmed-With: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>

Signed-off-by: Stefan Metzmacher <metze@samba.org>
Signed-off-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>

Conflict: NA
Reference: https://github.com/samba-team/samba/commit/1c5146ddfc736e9d790bd91f3124c6fba6847bb3
---
 source3/rpc_server/samr/srv_samr_chgpasswd.c | 110 +++++++++++++++++--
 1 file changed, 101 insertions(+), 9 deletions(-)

diff --git a/source3/rpc_server/samr/srv_samr_chgpasswd.c b/source3/rpc_server/samr/srv_samr_chgpasswd.c
index 6c0c0da0cfc..9afb8799aea 100644
--- a/source3/rpc_server/samr/srv_samr_chgpasswd.c
+++ b/source3/rpc_server/samr/srv_samr_chgpasswd.c
@@ -54,6 +54,7 @@
 #include "passdb.h"
 #include "auth.h"
 #include "lib/util/sys_rw.h"
+#include "lib/util/util_str_escape.h"
 #include "librpc/rpc/dcerpc_samr.h"
 
 #include "lib/crypto/gnutls_helpers.h"
@@ -1008,27 +1009,118 @@ static bool check_passwd_history(struct samu *sampass, const char *plaintext)
 /***********************************************************
 ************************************************************/
 
+static NTSTATUS check_password_complexity_internal(TALLOC_CTX *tosctx,
+						   const char *orig_cmd,
+						   const char *username,
+						   char **cmd_out)
+{
+	const char *fallback_username = "__CVE-2026-4408_FallbackUsername__";
+	const char *inv = NULL;
+	char *cmd = NULL;
+	bool modified = false;
+	bool masked = false;
+	bool mixed_fallback = false;
+
+	*cmd_out = NULL;
+
+	if (username == NULL) {
+		return NT_STATUS_INVALID_USER_PRINCIPAL_NAME;
+	}
+
+	/*
+	 * This catches invalid characters in account names
+	 * which might be problematic passing to a shell script.
+	 */
+	inv = strstr_for_invalid_account_characters(username);
+	if (inv != NULL) {
+		char *le_username = log_escape(tosctx, username);
+
+		DBG_WARNING("username '%s' has invalid or dangerous characters\n",
+			    le_username);
+
+		TALLOC_FREE(le_username);
+
+		return NT_STATUS_INVALID_USER_PRINCIPAL_NAME;
+	}
+
+	/*
+	 * This masks the remaining unsafe characters which
+	 * are not already caught by strstr_for_invalid_account_characters()
+	 * with '_'.
+	 *
+	 * Then it replaces %u with an single quoted
+	 * and/or shell escaped version of the masked username.
+	 */
+	cmd = talloc_string_sub_unsafe(tosctx,
+				       orig_cmd,
+				       'u',
+				       username,
+				       STRING_SUB_UNSAFE_CHARACTERS,
+				       '_',
+				       fallback_username,
+				       &modified,
+				       &masked,
+				       &mixed_fallback);
+	if (cmd == NULL) {
+		return NT_STATUS_NO_MEMORY;
+	}
+
+	/*
+	 * Now warn about unexpected values
+	 */
+
+	if (mixed_fallback) {
+		D_WARNING("CVE-2026-4408: "
+			  "strange quoting in 'check password script', "
+			  "falling back to replace %%u with %s, "
+			  "use testparm to fix the configuration\n",
+			  fallback_username);
+		D_WARNING("CVE-2026-4408: "
+			  "You should use '%%u', or SAMBA_CPS_ACCOUNT_NAME "
+			  "inside of 'check password script'.\n");
+	} else if (masked) {
+		char *le_username = log_escape(tosctx, username);
+
+		D_WARNING("CVE-2026-4408: "
+			  "replaced %%u with masked value instead of: %s\n",
+			  le_username);
+		D_WARNING("CVE-2026-4408: "
+			  "You should use SAMBA_CPS_ACCOUNT_NAME inside "
+			  "'check password script' instead of %%u.\n");
+
+		TALLOC_FREE(le_username);
+	}
+
+	*cmd_out = cmd;
+	return NT_STATUS_OK;
+}
+
+
 NTSTATUS check_password_complexity(const char *username,
 				   const char *fullname,
 				   const char *password,
 				   enum samPwdChangeReason *samr_reject_reason)
 {
+	int check_ret;
+	NTSTATUS status;
 	TALLOC_CTX *tosctx = talloc_tos();
 	const struct loadparm_substitution *lp_sub =
 		loadparm_s3_global_substitution();
-	int check_ret;
-	char *cmd;
+	const char *orig_cmd = NULL;
+	char *cmd = NULL;
 
-	/* Use external script to check password complexity */
-	if ((lp_check_password_script(tosctx, lp_sub) == NULL)
-	    || (*(lp_check_password_script(tosctx, lp_sub)) == '\0')){
+	orig_cmd = lp_check_password_script(tosctx, lp_sub);
+	if (orig_cmd == NULL || orig_cmd[0] == '\0') {
 		return NT_STATUS_OK;
 	}
 
-	cmd = talloc_string_sub(tosctx, lp_check_password_script(tosctx, lp_sub), "%u",
-				username);
-	if (!cmd) {
-		return NT_STATUS_PASSWORD_RESTRICTION;
+	/* note we don't use 'fullname' or 'password' here */
+	status = check_password_complexity_internal(tosctx,
+						    orig_cmd,
+						    username,
+						    &cmd);
+	if (!NT_STATUS_IS_OK(status)) {
+		return status;
 	}
 
 	check_ret = setenv("SAMBA_CPS_ACCOUNT_NAME", username, 1);
-- 
2.33.0