CVE-2026-4408
Description
A flaw was found in Samba. A remote attacker can exploit a misconfiguration in Samba file servers and classic domain controllers that use the "check password script" feature. If this script is configured with the %u substitution character, the client-controlled username is passed without proper escaping of shell meta-characters. This vulnerability allows an attacker to achieve remote command execution on the affected system. This issue primarily affects non-standard configurations where the "check password script" is used with %u and the samba-dcerpcd service is started as a system service.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A remote unauthenticated attacker can execute commands on Samba file servers and classic DCs via shell meta-character injection in the 'check password script' when %u is used without proper escaping.
Vulnerability
A flaw exists in Samba file servers and classic (non-AD) domain controllers that use the check password script feature with the %u substitution character. When a client initiates a password change or reset via the SAMR DCE/RPC service (SamValidatePasswordChange and SamValidatePasswordReset), the username supplied by the client is passed directly to the script without escaping shell meta-characters [1], [2]. This affects Samba configurations where check password script contains %u (especially without single quotes around it, e.g., '%u') and the samba-dcerpcd service is started as a system service (which requires setting rpc start on demand helpers to the non-default value no). Active Directory Domain Controllers are not affected because they do not expand the username via %u [2].
Exploitation
An attacker must be able to reach the SAMR service over NCACN_IP_TCP on a system running a Samba file server or classic domain controller with the vulnerable configuration (the check password script with %u and samba-dcerpcd running as a system service) [2]. No authentication is required. The attacker crafts a username containing shell meta-characters (e.g., backticks, semicolons, pipes) and sends a password change or reset request. The unescaped username is then executed as part of the check password script command line, achieving remote command execution [1], [2].
Impact
Successful exploitation allows a remote unauthenticated attacker to execute arbitrary operating system commands with the privileges of the samba-dcerpcd service, typically root or a high-privileged user. This leads to full compromise of the affected system: disclosure of sensitive data, modification or destruction of files, installation of malware, or further lateral movement within the network [1], [2].
Mitigation
As of the publication date (2026-05-28), no fixed version has been released [1], [2]. Administrators should immediately review whether the check password script is configured with %u. If possible, remove the %u substitution or enclose %u in single quotes (e.g., '%u') to reduce the attack surface, though command-line option injection remains possible [2]. Ensure that rpc start on demand helpers is set to the default value (yes) so that samba-dcerpcd is not started as a system service in a way that exposes the vulnerable code [2]. Active Directory Domain Controllers are not affected. This vulnerability is not known to be listed in CISA's Known Exploited Vulnerabilities (KEV) catalog as of this writing.
AI Insight generated on May 28, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2(expand)+ 1 more
- (no CPE)
- (no CPE)
Patches
18e6c532fbd577CVE-2026-4408: s3:samr-server: only allow _samr_ValidatePassword as DC
1 file changed · +8 −0
source3/rpc_server/samr/srv_samr_nt.c+8 −0 modified@@ -7485,6 +7485,14 @@ NTSTATUS _samr_ValidatePassword(struct pipes_struct *p, return NT_STATUS_ACCESS_DENIED; } + if (lp_server_role() <= ROLE_DOMAIN_MEMBER) { + /* + * We only want this on DCs + */ + p->fault_state = DCERPC_FAULT_ACCESS_DENIED; + return NT_STATUS_ACCESS_DENIED; + } + if (r->in.level < 1 || r->in.level > 3) { return NT_STATUS_INVALID_INFO_CLASS; }
2d699a700830CVE-2026-4408: docs-xml/smbdotconf: clarify '%u' in 'check password script'
1 file changed · +8 −2
docs-xml/smbdotconf/security/checkpasswordscript.xml+8 −2 modified@@ -20,8 +20,8 @@ <itemizedlist> <listitem><para> - SAMBA_CPS_ACCOUNT_NAME is always present and contains the sAMAccountName of user, - the is the same as the %u substitutions in the none AD DC case. + SAMBA_CPS_ACCOUNT_NAME is always present and contains the sAMAccountName of user. + It is the same as the '%u' substitutions in the non AD DC case. </para></listitem> <listitem><para> @@ -33,6 +33,12 @@ </para></listitem> </itemizedlist> + <para>Even on a non AD DC SAMBA_CPS_ACCOUNT_NAME is the preferred way to access the + account name, as it contains the raw value provided by the client. If that's not + possible you should use single quotes (directly) around %u, e.g. /path/to/somescript '%u', + see CVE-2026-4408 for more details. + </para> + <para>Note: In the example directory is a sample program called <command moreinfo="none">crackcheck</command> that uses cracklib to check the password quality.</para>
b6fe311a6ac4CVE-2026-4480/CVE-2026-4408: lib/util: add test_string_sub unittests
3 files changed · +1052 −0
lib/util/tests/test_string_sub.c+1044 −0 added@@ -0,0 +1,1044 @@ + +#include <stdarg.h> +#include <stddef.h> +#include <stdint.h> +#include <setjmp.h> +#include <sys/stat.h> +#include "replace.h" +#include <cmocka.h> +#include "talloc.h" + +#include "../substitute.h" + +/* set _DEBUG_VERBOSE to print more. */ +#define _DEBUG_VERBOSE + +#ifdef _DEBUG_VERBOSE +#define debug_message(...) print_message(__VA_ARGS__) +#else +#define debug_message(...) /* debug_message */ +#endif + + +static int setup_talloc_context(void **state) +{ + TALLOC_CTX *mem_ctx = talloc_new(NULL); + *state = mem_ctx; + return 0; +} + +static int teardown_talloc_context(void **state) +{ + TALLOC_CTX *mem_ctx = *state; + TALLOC_FREE(mem_ctx); + return 0; +} + +struct cmd_expansion { + const char *lp_cmd; + const char *username; + const char *result_cmd; + bool modified; + bool masked; + bool mixed_fallback; +}; + +static void _test_talloc_string_sub_unsafe(void **state, + struct cmd_expansion expansions[], + size_t n_expansions, + const char *unsafe_characters) +{ + TALLOC_CTX *mem_ctx = *state; + size_t i; + + for (i = 0; i < n_expansions; i++) { + struct cmd_expansion t = expansions[i]; + char *result_cmd = NULL; + bool masked; + bool mixed_fallback; + bool modified; + bool flags_correct; + bool mixed; + int cmp; + + mixed = talloc_string_sub_mixed_quoting(t.lp_cmd, 'u'); + + result_cmd = talloc_string_sub_unsafe(mem_ctx, + t.lp_cmd, + 'u', + t.username, + unsafe_characters, + '_', + "FallbackUsername", + &modified, + &masked, + &mixed_fallback); + assert_ptr_not_equal(result_cmd, NULL); + assert_ptr_not_equal(t.result_cmd, NULL); + + cmp = strcmp(t.result_cmd, result_cmd); + flags_correct = (modified == t.modified && + masked == t.masked && + mixed_fallback == t.mixed_fallback); + + if (cmp == 0) { + debug_message("[%zu] «%s» «%s» -> «%s»; AS EXPECTED\n", + i, t.lp_cmd, + t.username, + result_cmd); + } else { + debug_message("[%zu] «%s» «%s»; " + "expected [%zu] «%s» got [%zu] «%s»\033[1;31m BAD! \033[0m\n", + i, t.lp_cmd, + t.username, + strlen(t.result_cmd), t.result_cmd, + strlen(result_cmd), result_cmd); + } + assert_int_equal(cmp, 0); + if (!flags_correct) { + debug_message("[%zu] ", i); +#define _FLAG(x) debug_message((t. x == x) ? "%s: %s √; ": \ + "%s \033[1;31m expected %s \033[0m; ", \ + #x, t.x ? "true": "false"); + _FLAG(modified); + _FLAG(masked); + _FLAG(mixed_fallback); + debug_message("\n"); + } + assert_int_equal(flags_correct, true); + if (mixed_fallback != mixed) { + debug_message("[%zu] %s mixed \033[1;31m expected %s \033[0m; ", + i, t.lp_cmd, + mixed_fallback ? "true": "false"); + } + assert_int_equal(mixed_fallback, mixed); +#undef _FLAG + } + debug_message("ALL correct\n"); +} + +static void test_talloc_string_sub_unsafe(void **state) +{ + const char *unsafe_characters = STRING_SUB_UNSAFE_CHARACTERS; + + static struct cmd_expansion expansions[] = { + { + "/bin/echo \"bob'", + "bob", + "/bin/echo \"bob'", + false, + false, + false, + }, + { + "/bin/echo '%u'", + "bob", + "/bin/echo 'bob'", + true, + false, + false, + }, + { + "/bin/echo %u", + "bob", + "/bin/echo 'bob'", + true, + false, + false, + }, + { + "/bin/echo %u", + "bob'", + "/bin/echo 'bob_'", + true, + true, + false, + }, + { + "/bin/echo %u", + "bob'''", + "/bin/echo 'bob___'", + true, + true, + false, + }, + { + "/bin/echo %u", + "bob\'", + "/bin/echo 'bob_'", + true, + true, + false, + }, + { + "/bin/echo '%u", + "bob bob bob", + "/bin/echo 'FallbackUsername", + true, + false, + true, + }, + { + "/bin/echo \"%u\"", + " ", + "/bin/echo ' '", + true, + false, + false, + }, + { + "/bin/echo \"--uu=%u\"", + "bob", + "/bin/echo \"--uu=FallbackUsername\"", + true, + false, + true, + }, + { + "/bin/echo \"--uu=%u\"", + "bob !0", + "/bin/echo \"--uu=FallbackUsername\"", + true, + false, + true, + }, + { + "/bin/echo %u", + "!0", + "/bin/echo '!0'", + true, + false, + false, + }, + { + "/bin/echo \"--uu=%u\"", + "bob \\", + "/bin/echo \"--uu=FallbackUsername\"", + true, + false, + true, + }, + { + "/bin/echo --uu='%u'", + "bob >> x", + "/bin/echo --uu='bob __ x'", + true, + true, + false, + }, + { + "/bin/echo '--uu=%u\"", + "bob", + "/bin/echo '--uu=FallbackUsername\"", + true, + false, + true, + }, + { + "/bin/echo --uu='%u'", + "bob", + "/bin/echo --uu='bob'", + true, + false, + false, + }, + { + "/bin/echo --uu'=%u'", + "bob", + "/bin/echo --uu'=FallbackUsername'", + true, + false, + true, + }, + { + "/bin/echo --uu'=%u'", + "`ls`", + "/bin/echo --uu'=FallbackUsername'", + true, + true, + true, + }, + { + "/bin/echo --uu='%u'", + "u%u%u%u%u", + "/bin/echo --uu='u_u_u_u_u'", + true, + true, + false, + }, + { + "/bin/echo --uu='%u'", + "$(ls)", + "/bin/echo --uu='_(ls)'", + true, + true, + false, + }, + { + "/bin/echo --uu='%u'", + "`ls`", + "/bin/echo --uu='_ls_'", + true, + true, + false, + }, + { + "/bin/echo --uu='1' %u", + "`ls`", + "/bin/echo --uu='1' FallbackUsername", + true, + true, + true, + }, + { + "/bin/echo --uu=\"'%u'\"", + "bob", + "/bin/echo --uu=\"'bob'\"", + true, + false, + false, + }, + { + "/bin/echo --uu='%u' --yy='%u' '%u' %u", + "bob", + "/bin/echo --uu='bob' --yy='bob' 'bob' FallbackUsername", + true, + false, + true, + }, + { + "/bin/echo --uu=%u%u%u'' %user 50%u", + "bob", + "/bin/echo --uu=FallbackUsernameFallbackUsernameFallbackUsername'' FallbackUsernameser 50FallbackUsername", + true, + false, + true, + }, + { + "/bin/echo %u", + "!!", + "/bin/echo '!!'", + true, + false, + false, + }, + { + "/bin/echo %u", + ">xxx", + "/bin/echo '_xxx'", + true, + true, + false, + }, + { + "/bin/echo %u", + "3", + "/bin/echo '3'", + true, + false, + false, + }, + { + "/bin/echo '%u'", + "3$", + "/bin/echo '3_'", + true, + true, + false, + }, + { + "/bin/echo '%u'", + "comp$", + "/bin/echo 'comp_'", + true, + true, + false, + }, + { + "/bin/echo '%u'", + "3$3", + "/bin/echo '3_3'", + true, + true, + false, + }, + { + "/bin/echo '%u'", + "q $3", + "/bin/echo 'q _3'", + true, + true, + false, + }, + { + "/bin/echo '%u", + "q $3", + "/bin/echo 'FallbackUsername", + true, + true, + true, + }, + { + "/bin/echo -s '%u' %u", + "āāā", + "/bin/echo -s 'āāā' FallbackUsername", + true, + false, + true, + }, + { + "/bin/echo -s '%u' %u", + "-āāā", + "/bin/echo -s '_āāā' FallbackUsername", + true, + true, + true, + }, + { + "/bin/echo -s %u", + "āāā", + "/bin/echo -s 'āāā'", + true, + false, + false, + }, + { + "/bin/echo -s %u", + "a -a", + "/bin/echo -s 'a -a'", + true, + false, + false, + }, + { + "/bin/echo -s=%u %u", + "ā -a", + "/bin/echo -s='ā -a' 'ā -a'", + true, + false, + false, + }, + { + "/bin/echo -s=\"%u %u\"", + "ā -a", + "/bin/echo -s=\"FallbackUsername FallbackUsername\"", + true, + false, + true, + }, + { + "/bin/echo -m='fridge' %u", + "ā -ß", + "/bin/echo -m='fridge' FallbackUsername", + true, + false, + true, + }, + { + "/bin/echo -m='fridge' %u", + "-ā -a", + "/bin/echo -m='fridge' FallbackUsername", + true, + true, + true, + }, + { + "/bin/echo %u", + "-n", + "/bin/echo '_n'", + true, + true, + false, + }, + { + "/bin/echo %u", + "o'clock", + "/bin/echo 'o_clock'", + true, + true, + false, + }, + { + "/bin/echo \"bob'", + "bob", + "/bin/echo \"bob'", + false, + false, + false, + }, + { + "/bin/echo \"%u\"", + "%u", + "/bin/echo '_u'", + true, + true, + false, + }, + { + "/bin/echo \"$(ls)\"", + "%u", + "/bin/echo \"$(ls)\"", + false, + false, + false, + }, + { + "/bin/echo %u", + "\\", + "/bin/echo '\\'", + true, + false, + false, + }, + { + "/bin/echo '%u'", + "\\", + "/bin/echo '\\'", + true, + false, + false, + }, + { + "/bin/echo \"%u\"", + "\\", + "/bin/echo '\\'", + true, + false, + false, + }, + { + "/bin/echo \"%u\" %u", + "\\", + "/bin/echo '\\' FallbackUsername", + true, + false, + true, + }, + { + "/bin/echo '%u' \"%u\" %u", + "\\", + "/bin/echo '\\' \"FallbackUsername\" FallbackUsername", + true, + false, + true, + }, + { + "/bin/echo '%u' \"%u\"", + "bob", + "/bin/echo 'bob' \"FallbackUsername\"", + true, + false, + true, + }, + }; + + _test_talloc_string_sub_unsafe(state, + expansions, + ARRAY_SIZE(expansions), + unsafe_characters); +} + +static void test_talloc_string_sub_unsafe_minimal_unsafe_chars(void **state) +{ + const char *unsafe_characters = "\"'%"; + + static struct cmd_expansion expansions[] = { + { + "/bin/echo \"bob'", + "bob", + "/bin/echo \"bob'", + false, + false, + false, + }, + { + "/bin/echo '%u'", + "bob", + "/bin/echo 'bob'", + true, + false, + false, + }, + { + "/bin/echo %u", + "bob", + "/bin/echo 'bob'", + true, + false, + false, + }, + { + "/bin/echo %u", + "bob'", + "/bin/echo 'bob_'", + true, + true, + false, + }, + { + "/bin/echo %u", + "bob'''", + "/bin/echo 'bob___'", + true, + true, + false, + }, + { + "/bin/echo %u", + "bob\'", + "/bin/echo 'bob_'", + true, + true, + false, + }, + { + "/bin/echo '%u", + "bob bob bob", + "/bin/echo 'FallbackUsername", + true, + false, + true, + }, + { + "/bin/echo \"%u\"", + " ", + "/bin/echo ' '", + true, + false, + false, + }, + { + "/bin/echo \"--uu=%u\"", + "bob", + "/bin/echo \"--uu=FallbackUsername\"", + true, + false, + true, + }, + { + "/bin/echo \"--uu=%u\"", + "bob !0", + "/bin/echo \"--uu=FallbackUsername\"", + true, + false, + true, + }, + { + "/bin/echo %u", + "!0", + "/bin/echo '!0'", + true, + false, + false, + }, + { + "/bin/echo \"--uu=%u\"", + "bob \\", + "/bin/echo \"--uu=FallbackUsername\"", + true, + false, + true, + }, + { + "/bin/echo --uu='%u'", + "bob >> x", + "/bin/echo --uu='bob >> x'", + true, + false, + false, + }, + { + "/bin/echo '--uu=%u\"", + "bob", + "/bin/echo '--uu=FallbackUsername\"", + true, + false, + true, + }, + { + "/bin/echo --uu='%u'", + "bob", + "/bin/echo --uu='bob'", + true, + false, + false, + }, + { + "/bin/echo --uu'=%u'", + "bob", + "/bin/echo --uu'=FallbackUsername'", + true, + false, + true, + }, + { + "/bin/echo --uu'=%u'", + "`ls`", + "/bin/echo --uu'=FallbackUsername'", + true, + false, + true, + }, + { + "/bin/echo --uu='%u'", + "u%u%u%u%u", + "/bin/echo --uu='u_u_u_u_u'", + true, + true, + false, + }, + { + "/bin/echo --uu='%u'", + "$(ls)", + "/bin/echo --uu='$(ls)'", + true, + false, + false, + }, + { + "/bin/echo --uu='%u'", + "`ls`", + "/bin/echo --uu='`ls`'", + true, + false, + false, + }, + { + "/bin/echo --uu='1' %u", + "`ls`", + "/bin/echo --uu='1' FallbackUsername", + true, + false, + true, + }, + { + "/bin/echo --uu=\"'%u'\"", + "bob", + "/bin/echo --uu=\"'bob'\"", + true, + false, + false, + }, + { + "/bin/echo --uu='%u' --yy='%u' '%u' %u", + "bob", + "/bin/echo --uu='bob' --yy='bob' 'bob' FallbackUsername", + true, + false, + true, + }, + { + "/bin/echo --uu=%u%u%u'' %user 50%u", + "bob", + "/bin/echo --uu=FallbackUsernameFallbackUsernameFallbackUsername'' FallbackUsernameser 50FallbackUsername", + true, + false, + true, + }, + { + "/bin/echo %u", + "!!", + "/bin/echo '!!'", + true, + false, + false, + }, + { + "/bin/echo %u", + ">xxx", + "/bin/echo '>xxx'", + true, + false, + false, + }, + { + "/bin/echo %u", + "3", + "/bin/echo '3'", + true, + false, + false, + }, + { + "/bin/echo '%u'", + "3$", + "/bin/echo '3$'", + true, + false, + false, + }, + { + "/bin/echo '%u'", + "comp$", + "/bin/echo 'comp$'", + true, + false, + false, + }, + { + "/bin/echo '%u'", + "3$3", + "/bin/echo '3$3'", + true, + false, + false, + }, + { + "/bin/echo '%u'", + "q $3", + "/bin/echo 'q $3'", + true, + false, + false, + }, + { + "/bin/echo '%u", + "q $3", + "/bin/echo 'FallbackUsername", + true, + false, + true, + }, + { + "/bin/echo -s '%u' %u", + "āāā", + "/bin/echo -s 'āāā' FallbackUsername", + true, + false, + true, + }, + { + "/bin/echo -s '%u' %u", + "-āāā", + "/bin/echo -s '_āāā' FallbackUsername", + true, + true, + true, + }, + { + "/bin/echo -s %u", + "āāā", + "/bin/echo -s 'āāā'", + true, + false, + false, + }, + { + "/bin/echo -s %u", + "a -a", + "/bin/echo -s 'a -a'", + true, + false, + false, + }, + { + "/bin/echo -s=%u %u", + "ā -a", + "/bin/echo -s='ā -a' 'ā -a'", + true, + false, + false, + }, + { + "/bin/echo -s=\"%u %u\"", + "ā -a", + "/bin/echo -s=\"FallbackUsername FallbackUsername\"", + true, + false, + true, + }, + { + "/bin/echo -m='fridge' %u", + "ā -ß", + "/bin/echo -m='fridge' FallbackUsername", + true, + false, + true, + }, + { + "/bin/echo -m='fridge' %u", + "-ā -a", + "/bin/echo -m='fridge' FallbackUsername", + true, + true, + true, + }, + { + "/bin/echo %u", + "-n", + "/bin/echo '_n'", + true, + true, + false, + }, + { + "/bin/echo %u", + "o'clock", + "/bin/echo 'o_clock'", + true, + true, + false, + }, + { + "/bin/echo \"bob'", + "bob", + "/bin/echo \"bob'", + false, + false, + false, + }, + { + "/bin/echo \"%u\"", + "%u", + "/bin/echo '_u'", + true, + true, + false, + }, + { + "/bin/echo \"$(ls)\"", + "%u", + "/bin/echo \"$(ls)\"", + false, + false, + false, + }, + { + "/bin/echo %u", + "\\", + "/bin/echo '\\'", + true, + false, + false, + }, + { + "/bin/echo '%u'", + "\\", + "/bin/echo '\\'", + true, + false, + false, + }, + { + "/bin/echo \"%u\"", + "\\", + "/bin/echo '\\'", + true, + false, + false, + }, + { + "/bin/echo \"%u\" %u", + "\\", + "/bin/echo '\\' FallbackUsername", + true, + false, + true, + }, + { + "/bin/echo '%u' \"%u\" %u", + "\\", + "/bin/echo '\\' \"FallbackUsername\" FallbackUsername", + true, + false, + true, + }, + { + "/bin/echo '%u' \"%u\"", + "bob", + "/bin/echo 'bob' \"FallbackUsername\"", + true, + false, + true, + }, + }; + + _test_talloc_string_sub_unsafe(state, + expansions, + ARRAY_SIZE(expansions), + unsafe_characters); +} + +static void test_talloc_string_sub_unsafe_all_mixes(void **state) +{ + const char *unsafe_characters = STRING_SUB_UNSAFE_CHARACTERS; + size_t i; + + for (i = 0; i < 32; i++) { + char in[100] = { 0, }; + char out[100] = { 0, }; + struct cmd_expansion expansions[] = { + { + in, + "bob", + out, + true, + false, + false, + }, + }; + bool vsq = i & 1; + bool vdq = i & 2; + bool v = i & 4; + bool sq = i & 8; + bool dq = i & 16; + char *inp = in; + char *outp = out; + if (vsq) { + inp = stpcpy(inp, "'%u' "); + outp = stpcpy(outp, "'bob' "); + debug_message("vsq "); + } + if (vdq) { + inp = stpcpy(inp, "\"%u\" "); + outp = stpcpy(outp, (vsq || sq) ? "\"FallbackUsername\" " : "'bob' "); + debug_message("vdq "); + if (vsq || sq) { + expansions[0].mixed_fallback = true; + } + } + if (v) { + inp = stpcpy(inp, "%u "); + outp = stpcpy(outp, (vsq || vdq || sq || dq) ? "FallbackUsername " : "'bob' "); + debug_message("v "); + if (vsq || vdq || sq || dq) { + expansions[0].mixed_fallback = true; + } + } + if (sq) { + inp = stpcpy(inp, "' "); + outp = stpcpy(outp, "' "); + debug_message("sq "); + } + if (dq) { + inp = stpcpy(inp, "\" "); + outp = stpcpy(outp, "\" "); + debug_message("dq "); + } + debug_message("(i: %zu)\n", i); + *inp = '\0'; + *outp = '\0'; + expansions[0].modified = strcmp(in, out) != 0; + + _test_talloc_string_sub_unsafe(state, + expansions, + ARRAY_SIZE(expansions), + unsafe_characters); + } +} + + +int main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_talloc_string_sub_unsafe), + cmocka_unit_test(test_talloc_string_sub_unsafe_minimal_unsafe_chars), + cmocka_unit_test(test_talloc_string_sub_unsafe_all_mixes), + }; + if (!isatty(1)) { + cmocka_set_message_output(CM_OUTPUT_SUBUNIT); + } + return cmocka_run_group_tests(tests, + setup_talloc_context, + teardown_talloc_context); +}
lib/util/wscript_build+6 −0 modified@@ -427,3 +427,9 @@ else: deps='cmocka replace talloc stable_sort', local_include=False, for_selftest=True) + + bld.SAMBA3_BINARY('test_string_sub', + source='tests/test_string_sub.c', + deps='''cmocka replace talloc samba-util + ''', + for_selftest=True)
selftest/tests.py+2 −0 modified@@ -560,6 +560,8 @@ def cmdline(script, *args): [os.path.join(bindir(), "default/lib/util/test_json_logging")]) plantestsuite("samba.unittests.stable_sort", "none", [os.path.join(bindir(), "default/lib/util/test_stable_sort")]) +plantestsuite("samba.unittests.test_string_sub", "none", + [os.path.join(bindir(), "test_string_sub")]) plantestsuite("samba.unittests.ntlm_check", "none", [os.path.join(bindir(), "default/libcli/auth/test_ntlm_check")]) plantestsuite("samba.unittests.gnutls", "none",
a4b214e799b7CVE-2026-4408: s3:testparm: warn about 'check password script' %u usage
1 file changed · +12 −0
source3/utils/testparm.c+12 −0 modified@@ -384,6 +384,7 @@ static int do_global_checks(void) const char **lp_ptr = NULL; const struct loadparm_substitution *lp_sub = loadparm_s3_global_substitution(); + const char *check_pw_script = NULL; int ival; fprintf(stderr, "\n"); @@ -856,6 +857,17 @@ static int do_global_checks(void) #endif } + check_pw_script = lp_check_password_script(talloc_tos(), lp_sub); + if (talloc_string_sub_mixed_quoting(check_pw_script, 'u')) { + fprintf(stderr, + "WARNING: You are using 'check password script' " + "with mixed quoting and %%u.\n" + "CVE-2026-4408 changed the way %%u substitution works. \n" + "You should use the SAMBA_CPS_ACCOUNT_NAME " + "environment variable exported to the script, or\n" + "at least use single quotes (directly) around '%%u'.\n\n"); + } + return ret; }
88c45db50d22CVE-2026-4480/CVE-2026-4408: lib/util: let log_escape() make use of iscntrl()
1 file changed · +3 −2
lib/util/util_str_escape.c+3 −2 modified@@ -18,6 +18,7 @@ */ #include "replace.h" +#include "system/locale.h" #include "lib/util/debug.h" #include "lib/util/util_str_escape.h" @@ -28,7 +29,7 @@ */ static size_t encoded_length(unsigned char c) { - if (c != '\\' && c > 0x1F) { + if (c != '\\' && !iscntrl(c)) { return 1; } else { switch (c) { @@ -79,7 +80,7 @@ char *log_escape(TALLOC_CTX *frame, const char *in) c = in; e = encoded; while (*c) { - if (*c != '\\' && (unsigned char)(*c) > 0x1F) { + if (*c != '\\' && !iscntrl((unsigned char)(*c))) { *e++ = *c++; } else { switch (*c) {
c610d8c6b1efCVE-2026-4480/CVE-2026-4408: lib/util: add talloc_string_sub_{mixed_quoting,unsafe}() helpers
2 files changed · +277 −0
lib/util/substitute.c+260 −0 modified@@ -25,6 +25,8 @@ #include "system/locale.h" #include "debug.h" #ifndef SAMBA_UTIL_CORE_ONLY +#include "lib/util/fault.h" +#include "lib/util/talloc_stack.h" #include "charset/charset.h" #else #include "charset_compat.h" @@ -297,3 +299,261 @@ char *talloc_all_string_sub(TALLOC_CTX *ctx, return talloc_string_sub2(ctx, src, pattern, insert, false, false, false); } + +#ifndef SAMBA_UTIL_CORE_ONLY + +bool talloc_string_sub_mixed_quoting(const char *full_cmd, char variable_char) +{ + /* + * Try to make sure talloc_string_sub_unsafe() + * won't return NULL, instead talloc_stackframe_pool() + * would panic + */ + size_t cmd_len = full_cmd != NULL ? strlen(full_cmd) : 0; + size_t pool_size = 512 + cmd_len; + TALLOC_CTX *frame = talloc_stackframe_pool(pool_size); + char *cmd = NULL; + bool modified = false; + bool masked = false; + bool mixed_fallback = false; + + cmd = talloc_string_sub_unsafe(frame, + full_cmd, + variable_char, + "U", /* unsafe_value */ + "'\"%", /* unsafe_characters */ + '_', /* safe_character */ + "F", /* fallback_value */ + &modified, + &masked, + &mixed_fallback); + if (cmd == NULL) { + mixed_fallback = false; + } + TALLOC_FREE(frame); + return mixed_fallback; +} + +char *talloc_string_sub_unsafe(TALLOC_CTX *mem_ctx, + const char *orig_cmd, + char variable_char, + const char *unsafe_value, + const char *unsafe_characters, + char safe_character, + const char *fallback_value, + bool *_modified, + bool *_masked, + bool *_mixed_fallback) +{ + TALLOC_CTX *frame = talloc_stackframe(); + const char variable[3] = + { '%', variable_char, '\0' }; + const char variable_s_quoted[5] = + { '\'', '%', variable_char, '\'', '\0' }; + const char variable_d_quoted[5] = + { '"', '%', variable_char, '"', '\0' }; + char *cmd = NULL; + char *masked_value = NULL; + char *quoted_value = NULL; + bool has_s_quotes; + bool has_d_quotes; + bool has_variable; + bool has_variable_s_quoted; + bool has_variable_d_quoted; + bool modified = false; + bool masked = false; + bool mixed_fallback = false; + bool ok; + + /* + * The unsafe_characters argument should contain + * single and double quotes. + * Otherwise We can't safely handle this. + */ + SMB_ASSERT(unsafe_characters != NULL); + SMB_ASSERT(strchr(unsafe_characters, '\'') != NULL); + SMB_ASSERT(strchr(unsafe_characters, '"') != NULL); + SMB_ASSERT(strchr(unsafe_characters, '%') != NULL); + + cmd = talloc_strdup(mem_ctx, orig_cmd); + if (cmd == NULL) { + TALLOC_FREE(frame); + return NULL; + } + cmd = talloc_steal(frame, cmd); + + has_variable = strstr(orig_cmd, variable) != NULL; + if (!has_variable) { + /* + * Nothing to do... + */ + goto done; + } + modified = true; + + /* + * Replace all unsafe characters as well as control + * characters. + * + * Note that we start with masked_value = "%u" + * and then replace "%u" with unsafe_value, + * as a result we have a masked version of + * unsafe_value. + * + * And don't allow option injected like + * + * '-h value' + * '--help value' + * + */ + masked_value = talloc_strdup(frame, variable); + if (masked_value == NULL) { + goto nomem; + } + ok = realloc_string_sub_raw(&masked_value, + variable, + unsafe_value, + false, /* replace_once */ + false, /* allow_trailing_dollar */ + unsafe_characters, + safe_character); + if (!ok) { + goto nomem; + } + if (masked_value[0] == '-') { + masked_value[0] = safe_character; + } + masked = strcmp(masked_value, unsafe_value) != 0; + +retry: + + has_s_quotes = strchr(cmd, '\'') != NULL; + has_d_quotes = strchr(cmd, '"') != NULL; + has_variable = strstr(cmd, variable) != NULL; + has_variable_s_quoted = strstr(cmd, variable_s_quoted) != NULL; + has_variable_d_quoted = strstr(cmd, variable_d_quoted) != NULL; + + if (has_variable_s_quoted) { + /* + * In smb.conf we have something like + * + * some script = /usr/bin/script '%u' + * + * It is safe to replace '%u' (or '%J' etc, depending + * on variable_char) with '<masked_value>' if + * masked_value does not contain single quotes. We + * have checked that. + */ + + if (quoted_value == NULL) { + quoted_value = talloc_asprintf(frame, "'%s'", + masked_value); + if (quoted_value == NULL) { + goto nomem; + } + } + + ok = realloc_string_sub_raw(&cmd, + variable_s_quoted, + quoted_value, + false, /* replace_once */ + false, /* allow_trailing_dollar */ + NULL, /* unsafe_characters */ + '\0'); /* safe_character */ + if (!ok) { + goto nomem; + } + + goto retry; + } + + if (has_variable_d_quoted && !has_s_quotes) { + /* + * replace the "%u" + * + * some script = /usr/bin/script "%u" + * + * with '%u' and try the '%u' -> 'variable' substitution + * again. + */ + + ok = realloc_string_sub_raw(&cmd, + variable_d_quoted, + variable_s_quoted, + false, /* replace_once */ + false, /* allow_trailing_dollar */ + NULL, /* unsafe_characters */ + '\0'); /* safe_character */ + if (!ok) { + goto nomem; + } + + goto retry; + } + + if (has_variable && !has_s_quotes && !has_d_quotes) { + /* + * In this case: + * + * some script = /usr/bin/script %u + * + * we can safely substitute %u -> '%u' and try the + * single quote test again. + */ + + ok = realloc_string_sub_raw(&cmd, + variable, + variable_s_quoted, + false, /* replace_once */ + false, /* allow_trailing_dollar */ + NULL, /* unsafe_characters */ + '\0'); /* safe_character */ + if (!ok) { + goto nomem; + } + + goto retry; + } + + if (has_variable) { + /* + * There are single or double quotes, but not tightly + * bound around a %u. + * + * Or there's a mix of single and double quotes. + * + * We just use a generic fallback value. + * and let the caller warn about this + * and give the admin a hind to fix the smb.conf + * option. + */ + mixed_fallback = true; + + ok = realloc_string_sub_raw(&cmd, + variable, + fallback_value, + false, /* replace_once */ + false, /* allow_trailing_dollar */ + NULL, /* unsafe_characters */ + '\0'); /* safe_character */ + if (!ok) { + goto nomem; + } + } + +done: + *_modified = modified; + *_masked = masked; + *_mixed_fallback = mixed_fallback; + cmd = talloc_steal(mem_ctx, cmd); + TALLOC_FREE(frame); + return cmd; + +nomem: + *_modified = false; + *_masked = false; + *_mixed_fallback = false; + TALLOC_FREE(frame); + return NULL; +} +#endif /* ! SAMBA_UTIL_CORE_ONLY */
lib/util/substitute.h+17 −0 modified@@ -83,4 +83,21 @@ char *talloc_all_string_sub(TALLOC_CTX *ctx, const char *src, const char *pattern, const char *insert); + +#ifndef SAMBA_UTIL_CORE_ONLY +bool talloc_string_sub_mixed_quoting(const char *full_cmd, char variable_char); + +char *talloc_string_sub_unsafe(TALLOC_CTX *mem_ctx, + const char *orig_cmd, + char variable_char, + const char *unsafe_value, + const char *unsafe_characters, + char safe_character, + const char *fallback_value, + bool *_modified, + bool *_masked, + bool *_mixed_fallback); + +#endif /* ! SAMBA_UTIL_CORE_ONLY */ + #endif /* _SAMBA_SUBSTITUTE_H_ */
73231db51394CVE-2026-4480/CVE-2026-4408: s3:lib: fix potential memory leak in talloc_sub_basic()
1 file changed · +2 −0
source3/lib/substitute.c+2 −0 modified@@ -317,6 +317,7 @@ char *talloc_sub_basic(TALLOC_CTX *mem_ctx, } tmp_ctx = talloc_stackframe(); + a_string = talloc_steal(tmp_ctx, a_string); for (s = a_string; (p = strchr_m(s, '%')); s = a_string + (p - b)) { @@ -478,6 +479,7 @@ char *talloc_sub_basic(TALLOC_CTX *mem_ctx, TALLOC_FREE(a_string); done: + a_string = talloc_steal(mem_ctx, a_string); TALLOC_FREE(tmp_ctx); return a_string; }
288b82f7da96CVE-2026-4408: s3:torture: tests for password complexity scripts
3 files changed · +366 −0
selftest/tests.py+2 −0 modified@@ -576,6 +576,8 @@ def cmdline(script, *args): [os.path.join(bindir(), "default/source4/utils/oLschema2ldif/test_oLschema2ldif")]) plantestsuite("samba.unittests.auth.sam", "none", [os.path.join(bindir(), "test_auth_sam")]) +plantestsuite("samba.unittests.test_rpc_samr", "none", + [os.path.join(bindir(), "test_rpc_samr")]) if have_heimdal_support and not using_system_gssapi: plantestsuite("samba.unittests.auth.heimdal_gensec_unwrap_des", "none", [valgrindify(os.path.join(bindir(), "test_heimdal_gensec_unwrap_des"))])
source3/torture/test_rpc_samr.c+358 −0 added@@ -0,0 +1,358 @@ + +#include <stdarg.h> +#include <stddef.h> +#include <stdint.h> +#include <setjmp.h> +#include <sys/stat.h> +#include <cmocka.h> +#include "includes.h" +#include "talloc.h" +#include "libcli/util/ntstatus.h" +#include "../librpc/gen_ndr/samr.h" +#include "rpc_server/samr/srv_samr_util.h" + +/* set SAMR_DEBUG_VERBOSE to true to print more. */ +#define SAMR_DEBUG_VERBOSE true + +#if SAMR_DEBUG_VERBOSE +#define debug_message(...) print_message(__VA_ARGS__) +#else +#define debug_message(...) /* debug_message */ +#endif + +static int setup_talloc_context(void **state) +{ + TALLOC_CTX *mem_ctx = talloc_new(NULL); + *state = mem_ctx; + return 0; +} + +static int teardown_talloc_context(void **state) +{ + TALLOC_CTX *mem_ctx = *state; + TALLOC_FREE(mem_ctx); + return 0; +} + +struct cmd_expansion { + const char *lp_cmd; + const char *username; + const char *result_cmd; + NTSTATUS result_code; +}; + +static struct cmd_expansion expansions[] = { + { + "/bin/echo '%u'", + "bob", + "/bin/echo 'bob'", + NT_STATUS_OK + }, + { + "/bin/echo %u", + "bob", + "/bin/echo 'bob'", + NT_STATUS_OK + }, + { + "/bin/echo %u", + "bob'", + "/bin/echo 'bob_'", + NT_STATUS_OK + }, + { + "/bin/echo %u", + "bob\'", + "/bin/echo 'bob_'", + NT_STATUS_OK + }, + { + "/bin/echo %u", + "bob'''", + "/bin/echo 'bob___'", + NT_STATUS_OK + }, + { + "/bin/echo %u", + "bob*", + NULL, + NT_STATUS_INVALID_USER_PRINCIPAL_NAME + }, + { + "/bin/echo %u", + "bob\"", + NULL, + NT_STATUS_INVALID_USER_PRINCIPAL_NAME + }, + { + "/bin/echo '%u", + "bob bob bob", + "/bin/echo '__CVE-2026-4408_FallbackUsername__", + NT_STATUS_OK + }, + { + "/bin/echo \"%u\"", + " ", + "/bin/echo ' '", + NT_STATUS_OK + }, + { + "/bin/echo \"--uu=%u\"", + "bob", + "/bin/echo \"--uu=__CVE-2026-4408_FallbackUsername__\"", + NT_STATUS_OK + }, + { + "/bin/echo \"--uu=%u\"", + "bob !0", + "/bin/echo \"--uu=__CVE-2026-4408_FallbackUsername__\"", + NT_STATUS_OK + }, + { + "/bin/echo %u", + "!0", + "/bin/echo '!0'", + NT_STATUS_OK + }, + { + "/bin/echo \"--uu=%u\"", + "bob \\", + NULL, + NT_STATUS_INVALID_USER_PRINCIPAL_NAME + }, + { + "/bin/echo --uu='%u'", + "bob >> x", + NULL, + NT_STATUS_INVALID_USER_PRINCIPAL_NAME + }, + { + "/bin/echo '--uu=%u\"", + "bob", + "/bin/echo '--uu=__CVE-2026-4408_FallbackUsername__\"", + NT_STATUS_OK + }, + { + "/bin/echo --uu='%u'", + "bob", + "/bin/echo --uu='bob'", + NT_STATUS_OK + }, + { + "/bin/echo --uu'=%u'", + "bob", + "/bin/echo --uu'=__CVE-2026-4408_FallbackUsername__'", + NT_STATUS_OK + }, + { + "/bin/echo --uu'=%u'", + "`ls`", + "/bin/echo --uu'=__CVE-2026-4408_FallbackUsername__'", + NT_STATUS_OK + }, + { + "/bin/echo --uu'=%u'", + "$(ls)", + "/bin/echo --uu'=__CVE-2026-4408_FallbackUsername__'", + NT_STATUS_OK + }, + { + "/bin/echo --uu='%u'", + "$(ls)", + "/bin/echo --uu='_(ls)'", + NT_STATUS_OK + }, + { + "/bin/echo --uu=\"'%u'\"", + "bob", + "/bin/echo --uu=\"'bob'\"", + NT_STATUS_OK + }, + { + "/bin/echo --uu='%u' --yy='%u' '%u' %u", + "bob", + "/bin/echo --uu='bob' --yy='bob' 'bob' __CVE-2026-4408_FallbackUsername__", + NT_STATUS_OK + }, + { + "/bin/echo --uu=%u%u'' %user 50%u", + "bob", + "/bin/echo --uu=__CVE-2026-4408_FallbackUsername____CVE-2026-4408_FallbackUsername__'' __CVE-2026-4408_FallbackUsername__ser 50__CVE-2026-4408_FallbackUsername__", + NT_STATUS_OK + }, + { + "/bin/echo %u", + "!!", + "/bin/echo '!!'", + NT_STATUS_OK + }, + { + "/bin/echo %u", + ">xxx", + NULL, + NT_STATUS_INVALID_USER_PRINCIPAL_NAME + }, + { + "/bin/echo %u", + "\\", + NULL, + NT_STATUS_INVALID_USER_PRINCIPAL_NAME + }, + { + "/bin/echo %u", + "3", + "/bin/echo '3'", + NT_STATUS_OK + }, + { + "/bin/echo '%u'", + "3$", + "/bin/echo '3_'", + NT_STATUS_OK + }, + { + "/bin/echo '%u'", + "comp$", + "/bin/echo 'comp_'", + NT_STATUS_OK + }, + { + "/bin/echo '%u'", + "3$3", + "/bin/echo '3_3'", + NT_STATUS_OK + }, + { + "/bin/echo '%u'", + "q $3", + "/bin/echo 'q _3'", + NT_STATUS_OK + }, + { + "/bin/echo -s '%u' %u", + "āāā", + "/bin/echo -s 'āāā' __CVE-2026-4408_FallbackUsername__", + NT_STATUS_OK + }, + { + "/bin/echo -s '%u' %u", + "-āāā", + "/bin/echo -s '_āāā' __CVE-2026-4408_FallbackUsername__", + NT_STATUS_OK + }, + { + "/bin/echo -s %u", + "āāā", + "/bin/echo -s 'āāā'", + NT_STATUS_OK + }, + { + "/bin/echo -s %u", + "a -a", + "/bin/echo -s 'a -a'", + NT_STATUS_OK + }, + { + "/bin/echo -s=%u %u", + "ā -a", + "/bin/echo -s='ā -a' 'ā -a'", + NT_STATUS_OK + }, + { + "/bin/echo -s=\"%u %u\"", + "ā -a", + "/bin/echo -s=\"__CVE-2026-4408_FallbackUsername__ __CVE-2026-4408_FallbackUsername__\"", + NT_STATUS_OK + }, + { + "/bin/echo -m='fridge' %u", + "ā -x -ß", + "/bin/echo -m='fridge' __CVE-2026-4408_FallbackUsername__", + NT_STATUS_OK + }, + { + "/bin/echo -m='fridge' %u", + "-ā -a", + "/bin/echo -m='fridge' __CVE-2026-4408_FallbackUsername__", + NT_STATUS_OK + }, + { + "/bin/echo %u", + "-n", + "/bin/echo '_n'", + NT_STATUS_OK + }, + { + "/bin/echo %u", + "o'clock", + "/bin/echo 'o_clock'", + NT_STATUS_OK + }, +}; + +static void test_expansions(void **state) +{ + TALLOC_CTX *mem_ctx = *state; + size_t i; + + for (i = 0; i < ARRAY_SIZE(expansions); i++) { + struct cmd_expansion t = expansions[i]; + char *result_cmd = NULL; + NTSTATUS status; + + status = check_password_complexity_internal(mem_ctx, + t.lp_cmd, + t.username, + &result_cmd); + if (NT_STATUS_IS_OK(t.result_code) && NT_STATUS_IS_OK(status)) { + int cmp; + + cmp = strcmp(t.result_cmd, result_cmd); + if (cmp == 0) { + debug_message("[%zu] «%s» «%s» -> «%s», nstatus %s; AS EXPECTED\n", + i, t.lp_cmd, + t.username, + result_cmd, + nt_errstr(status)); + } else { + debug_message("[%zu] «%s» «%s», nstatus %s; " + "expected «%s» got «%s»\033[1;31m BAD! \033[0m\n", + i, t.lp_cmd, + t.username, + nt_errstr(status), + t.result_cmd, + result_cmd); + } + assert_int_equal(cmp, 0); + } else if (NT_STATUS_EQUAL(status, t.result_code)) { + debug_message("[%zu] «%s» «%s», nstatus %s FAILED AS EXPECTED\n", + i, t.lp_cmd, + t.username, + nt_errstr(status)); + } else { + debug_message("[%zu] «%s» «%s» -> «%s», nstatus %s; " + "EXPECTED result «%s» ntstatus %s; \033[1;31m BAD! \033[0m\n", + i, t.lp_cmd, + t.username, + result_cmd, + nt_errstr(status), + t.result_cmd, + nt_errstr(t.result_code)); + assert_int_equal(true, false); + } + } + debug_message("ALL correct\n"); +} + +int main(void) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_expansions), + }; + if (!isatty(1)) { + cmocka_set_message_output(CM_OUTPUT_SUBUNIT); + } + return cmocka_run_group_tests(tests, + setup_talloc_context, + teardown_talloc_context); +}
source3/torture/wscript_build+6 −0 modified@@ -133,3 +133,9 @@ bld.SAMBA3_BINARY('vfstest', SMBREADLINE ''', for_selftest=True) + +bld.SAMBA3_BINARY('test_rpc_samr', + source='test_rpc_samr.c', + deps='''RPC_SERVICE cmocka + ''', + for_selftest=True)
a54ef87bcdb3CVE-2026-4408: s3:samr-server: make check_password_complexity_internal() non-static, for easier testing
2 files changed · +9 −4
source3/rpc_server/samr/srv_samr_chgpasswd.c+4 −4 modified@@ -1009,10 +1009,10 @@ 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) +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;
source3/rpc_server/samr/srv_samr_util.h+5 −0 modified@@ -79,6 +79,11 @@ NTSTATUS pass_oem_change(char *user, const char *rhost, uchar password_encrypted_with_nt_hash[516], const uchar old_nt_hash_encrypted[16], enum samPwdChangeReason *reject_reason); + +NTSTATUS check_password_complexity_internal(TALLOC_CTX *mem_ctx, + const char *_orig_cmd, + const char *username, + char **cmd_out); NTSTATUS check_password_complexity(const char *username, const char *fullname, const char *password,
094852887ae6CVE-2026-4480/CVE-2026-4408: lib/util: split out realloc_string_sub_raw()
2 files changed · +78 −25
lib/util/substitute.c+60 −25 modified@@ -171,32 +171,24 @@ _PUBLIC_ void all_string_sub(char *s,const char *pattern,const char *insert, siz * talloc version of string_sub2. */ -char *talloc_string_sub2(TALLOC_CTX *mem_ctx, const char *src, - const char *pattern, - const char *insert, - bool remove_unsafe_characters, - bool replace_once, - bool allow_trailing_dollar) +bool realloc_string_sub_raw(char **_string, + const char *pattern, + const char *insert, + bool replace_once, + bool allow_trailing_dollar, + const char *unsafe_characters, + char safe_character) { - const char *unsafe_characters = STRING_SUB_UNSAFE_CHARACTERS; - const char safe_character = '_'; - char *p = NULL, + char *p = NULL; char *s = NULL; char *string = NULL; ssize_t ls,lp,li,ld, i; - if (!insert || !pattern || !*pattern || !src) { - return NULL; - } - - string = talloc_strdup(mem_ctx, src); - if (string == NULL) { - DEBUG(0, ("talloc_string_sub2: " - "talloc_strdup failed\n")); - return NULL; + if (!insert || !pattern || !*pattern || !_string|| !*_string) { + return false; } - s = string; + s = string = *_string; ls = (ssize_t)strlen(s); lp = (ssize_t)strlen(pattern); @@ -205,14 +197,13 @@ char *talloc_string_sub2(TALLOC_CTX *mem_ctx, const char *src, while ((p = strstr_m(s,pattern))) { if (ld > 0) { - int offset = PTR_DIFF(s,string); - string = (char *)talloc_realloc_size(mem_ctx, string, - ls + ld + 1); + ptrdiff_t offset = PTR_DIFF(s,string); + string = talloc_realloc(NULL, string, char, ls + ld + 1); if (!string) { - DEBUG(0, ("talloc_string_sub: out of " - "memory!\n")); - return NULL; + DBG_ERR("out of memory(realloc)!\n"); + return false; } + *_string = string; p = string + offset + (p - s); } if (li != lp) { @@ -234,6 +225,50 @@ char *talloc_string_sub2(TALLOC_CTX *mem_ctx, const char *src, break; } } + return true; +} + +char *talloc_string_sub2(TALLOC_CTX *mem_ctx, + const char *src, + const char *pattern, + const char *insert, + bool remove_unsafe_characters, + bool replace_once, + bool allow_trailing_dollar) +{ + const char *unsafe_characters = NULL; + char safe_character = '\0'; + char *string = NULL; + bool ok; + + if (!insert || !pattern || !*pattern || !src) { + return NULL; + } + + if (remove_unsafe_characters) { + unsafe_characters = STRING_SUB_UNSAFE_CHARACTERS; + safe_character = '_'; + } + + string = talloc_strdup(mem_ctx, src); + if (string == NULL) { + DBG_ERR("out of memory, talloc_strdup(src)!\n"); + return NULL; + } + + ok = realloc_string_sub_raw(&string, + pattern, + insert, + replace_once, + allow_trailing_dollar, + unsafe_characters, + safe_character); + if (!ok) { + TALLOC_FREE(string); + DBG_ERR("out of memory, realloc_string_sub_raw()!\n"); + return NULL; + } + return string; }
lib/util/substitute.h+18 −0 modified@@ -51,6 +51,24 @@ void string_sub(char *s,const char *pattern, const char *insert, size_t len); **/ void all_string_sub(char *s,const char *pattern,const char *insert, size_t len); +/* + * If unsafe_characters is NULL all characters are allowed, + * if unsafe_characters is not NULL all characters caught + * by iscntrl() are also replaced by safe_character. + * + * *_string might be reallocated! + * + * On error *_string may still be reallocated and + * may contain partial replacements. + */ +bool realloc_string_sub_raw(char **_string, + const char *pattern, + const char *insert, + bool replace_once, + bool allow_trailing_dollar, + const char *unsafe_characters, + char safe_character); + char *talloc_string_sub2(TALLOC_CTX *mem_ctx, const char *src, const char *pattern, const char *insert,
f6a9447df1c4CVE-2026-4480/CVE-2026-4408: s3:lib: let realloc_string_sub2() use realloc_string_sub_raw()
1 file changed · +24 −57
source3/lib/substitute_generic.c+24 −57 modified@@ -37,71 +37,38 @@ char *realloc_string_sub2(char *string, bool remove_unsafe_characters, bool allow_trailing_dollar) { - char *p, *in; - char *s; - ssize_t ls,lp,li,ld, i; + const char *unsafe_characters = NULL; + char safe_character = '\0'; + bool ok; if (!insert || !pattern || !*pattern || !string || !*string) return NULL; - s = string; + if (remove_unsafe_characters) { + unsafe_characters = STRING_SUB_UNSAFE_CHARACTERS; + safe_character = '_'; + } - in = talloc_strdup(talloc_tos(), insert); - if (!in) { - DEBUG(0, ("realloc_string_sub: out of memory!\n")); + ok = realloc_string_sub_raw(&string, + pattern, + insert, + false, /* replace_once */ + allow_trailing_dollar, + unsafe_characters, + safe_character); + if (!ok) { + DBG_ERR("out of memory, realloc_string_sub_raw()!\n"); + /* + * The calling convention of realloc_string_sub2() + * is very strange regarding stale string pointers. + * + * It is assumed the given string was allocated + * on talloc_tos(), so we just don't touch + * it at all here... + */ return NULL; } - ls = (ssize_t)strlen(s); - lp = (ssize_t)strlen(pattern); - li = (ssize_t)strlen(insert); - ld = li - lp; - for (i=0;i<li;i++) { - switch (in[i]) { - case '$': - /* allow a trailing $ - * (as in machine accounts) */ - if (allow_trailing_dollar && (i == li - 1 )) { - break; - } - FALL_THROUGH; - case '`': - case '"': - case '\'': - case ';': - case '%': - case '\r': - case '\n': - if ( remove_unsafe_characters ) { - in[i] = '_'; - break; - } - FALL_THROUGH; - default: - /* ok */ - break; - } - } - while ((p = strstr_m(s,pattern))) { - if (ld > 0) { - int offset = PTR_DIFF(s,string); - string = talloc_realloc(NULL, string, char, ls + ld + 1); - if (!string) { - DEBUG(0, ("realloc_string_sub: " - "out of memory!\n")); - talloc_free(in); - return NULL; - } - p = string + offset + (p - s); - } - if (li != lp) { - memmove(p+li,p+lp,strlen(p+lp)+1); - } - memcpy(p, in, li); - s = p + li; - ls += ld; - } - talloc_free(in); return string; }
9910bbfc09baCVE-2026-4408: lib/util: introduce strstr_for_invalid_account_characters()
2 files changed · +47 −0
lib/util/samba_util.h+9 −0 modified@@ -294,6 +294,15 @@ _PUBLIC_ size_t ascii_len_n(const char *src, size_t n); **/ _PUBLIC_ bool set_boolean(const char *boolean_string, bool *boolean); +/** + * Returns a pointer to the first invalid character in name. + * + * Passing a NULL pointer as name is not allowed! + * + * This returns NULL for a valid account name. + **/ +_PUBLIC_ const char *strstr_for_invalid_account_characters(const char *name); + /** * Convert a size specification like 16K into an integral number of bytes. **/
lib/util/util_str.c+38 −0 modified@@ -218,3 +218,41 @@ _PUBLIC_ bool set_boolean(const char *boolean_string, bool *boolean) } return false; } + +_PUBLIC_ const char *strstr_for_invalid_account_characters(const char *name) +{ + /* + * Return a pointer to the first invalid character in the + * sAMAccountName, or NULL if the whole name is valid. + * + * The rules here are based on + * + * https://social.technet.microsoft.com/wiki/contents/articles/11216.active-directory-requirements-for-creating-objects.aspx + */ + size_t i; + + for (i = 0; name[i] != '\0'; i++) { + uint8_t c = name[i]; + const char *p = NULL; + + if (iscntrl(c)) { + return &name[i]; + } + + p = strchr("\"[]:;|=+*?<>/\\,", c); + if (p != NULL) { + return &name[i]; + } + } + + if (i == 0) { + return &name[i]; + } + + if (name[i - 1] == '.') { + i -= 1; + return &name[i]; + } + + return NULL; +}
829001451397CVE-2026-4480/CVE-2026-4408: lib/util: let mask_unsafe_character() check all control characters
2 files changed · +10 −4
lib/util/substitute.c+7 −1 modified@@ -22,6 +22,7 @@ */ #include "replace.h" +#include "system/locale.h" #include "debug.h" #ifndef SAMBA_UTIL_CORE_ONLY #include "charset/charset.h" @@ -53,6 +54,10 @@ char mask_unsafe_character(char in, return in; } + if (iscntrl(in)) { + return safe_out; + } + unsafe = strchr(unsafe_characters, in); if (unsafe != NULL) { return safe_out; @@ -69,7 +74,8 @@ char mask_unsafe_character(char in, This routine looks for pattern in s and replaces it with insert. It may do multiple replacements or just one. - Any of STRING_SUB_UNSAFE_CHARACTERS in the insert string are replaced with _ + Any of STRING_SUB_UNSAFE_CHARACTERS and any character + caught by calling iscntrl() in the insert string are replaced with _ if len==0 then the string cannot be extended. This is different from the old use of len==0 which was for no length checks to be done.
lib/util/substitute.h+3 −3 modified@@ -26,7 +26,7 @@ #include <talloc.h> -#define STRING_SUB_UNSAFE_CHARACTERS "$`\"';%\r\n" +#define STRING_SUB_UNSAFE_CHARACTERS "$`\"';%" /** Substitute a string for a pattern in another string. Make sure there is @@ -35,8 +35,8 @@ This routine looks for pattern in s and replaces it with insert. It may do multiple replacements. - Any of STRING_SUB_UNSAFE_CHARACTERS (see above) in the - insert string are replaced with _ + Any of STRING_SUB_UNSAFE_CHARACTERS (see above) and any character + caught by calling iscntrl() in the insert string are replaced with _ if len==0 then the string cannot be extended. This is different from the old use of len==0 which was for no length checks to be done.
c51b42fae63eCVE-2026-4480/CVE-2026-4408: lib/util: add more unsafe characters to STRING_SUB_UNSAFE_CHARACTERS
1 file changed · +1 −1
lib/util/substitute.h+1 −1 modified@@ -26,7 +26,7 @@ #include <talloc.h> -#define STRING_SUB_UNSAFE_CHARACTERS "$`\"';%" +#define STRING_SUB_UNSAFE_CHARACTERS "$`\"';%|&<>" /** Substitute a string for a pattern in another string. Make sure there is
93c98023f51dCVE-2026-4480/CVE-2026-4408: lib/util: remove unused talloc_strdup(insert) from talloc_string_sub2()
1 file changed · +25 −32
lib/util/substitute.c+25 −32 modified@@ -157,7 +157,7 @@ char *talloc_string_sub2(TALLOC_CTX *mem_ctx, const char *src, bool replace_once, bool allow_trailing_dollar) { - char *p, *in; + char *p; char *s; char *string; ssize_t ls,lp,li,ld, i; @@ -175,22 +175,32 @@ char *talloc_string_sub2(TALLOC_CTX *mem_ctx, const char *src, s = string; - in = talloc_strdup(mem_ctx, insert); - if (!in) { - DEBUG(0, ("talloc_string_sub2: ENOMEM\n")); - talloc_free(string); - return NULL; - } ls = (ssize_t)strlen(s); lp = (ssize_t)strlen(pattern); li = (ssize_t)strlen(insert); ld = li - lp; - for (i=0;i<li;i++) { - switch (in[i]) { + while ((p = strstr_m(s,pattern))) { + if (ld > 0) { + int offset = PTR_DIFF(s,string); + string = (char *)talloc_realloc_size(mem_ctx, string, + ls + ld + 1); + if (!string) { + DEBUG(0, ("talloc_string_sub: out of " + "memory!\n")); + return NULL; + } + p = string + offset + (p - s); + } + if (li != lp) { + memmove(p+li,p+lp,strlen(p+lp)+1); + } + for (i=0; i<li; i++) { + switch (insert[i]) { case '$': - /* allow a trailing $ - * (as in machine accounts) */ + /* + * allow a trailing $ (as in machine accounts) + */ if (allow_trailing_dollar && (i == li - 1 )) { break; } @@ -204,42 +214,25 @@ char *talloc_string_sub2(TALLOC_CTX *mem_ctx, const char *src, case '\r': case '\n': if (remove_unsafe_characters) { - in[i] = '_'; - break; + p[i] = '_'; + continue; } FALL_THROUGH; default: /* ok */ break; - } - } - - while ((p = strstr_m(s,pattern))) { - if (ld > 0) { - int offset = PTR_DIFF(s,string); - string = (char *)talloc_realloc_size(mem_ctx, string, - ls + ld + 1); - if (!string) { - DEBUG(0, ("talloc_string_sub: out of " - "memory!\n")); - TALLOC_FREE(in); - return NULL; } - p = string + offset + (p - s); - } - if (li != lp) { - memmove(p+li,p+lp,strlen(p+lp)+1); + + p[i] = insert[i]; } - memcpy(p, in, li); s = p + li; ls += ld; if (replace_once) { break; } } - TALLOC_FREE(in); return string; }
45431b969e18CVE-2026-4480/CVE-2026-4408: lib/util: inline string_sub2() into string_sub() the only caller
1 file changed · +2 −18
lib/util/substitute.c+2 −18 modified@@ -47,10 +47,9 @@ use of len==0 which was for no length checks to be done. **/ -static void string_sub2(char *s,const char *pattern, const char *insert, size_t len, - bool remove_unsafe_characters, bool replace_once, - bool allow_trailing_dollar) +void string_sub(char *s, const char *pattern, const char *insert, size_t len) { + bool remove_unsafe_characters = true; char *p; size_t ls, lp, li, i; @@ -79,13 +78,6 @@ static void string_sub2(char *s,const char *pattern, const char *insert, size_t for (i=0;i<li;i++) { switch (insert[i]) { case '$': - /* allow a trailing $ - * (as in machine accounts) */ - if (allow_trailing_dollar && (i == li - 1 )) { - p[i] = insert[i]; - break; - } - FALL_THROUGH; case '`': case '"': case '\'': @@ -107,17 +99,9 @@ static void string_sub2(char *s,const char *pattern, const char *insert, size_t } s = p + li; ls = ls + li - lp; - - if (replace_once) - break; } } -void string_sub(char *s,const char *pattern, const char *insert, size_t len) -{ - string_sub2( s, pattern, insert, len, true, false, false ); -} - /** Similar to string_sub() but allows for any character to be substituted. Use with caution!
bd05bcd18a0cCVE-2026-4480/CVE-2026-4408: lib/util: factor out a mask_unsafe_character() helper function
2 files changed · +60 −55
lib/util/substitute.c+55 −54 modified@@ -35,21 +35,50 @@ * @brief Substitute utilities. **/ +static inline +char mask_unsafe_character(char in, + bool is_last, + bool allow_trailing_dollar, + const char *unsafe_characters, + char safe_out) +{ + const char *unsafe = NULL; + + if (unsafe_characters == NULL) { + return in; + } + + /* allow a trailing $ (as in machine accounts) */ + if (allow_trailing_dollar && is_last && in == '$') { + return in; + } + + unsafe = strchr(unsafe_characters, in); + if (unsafe != NULL) { + return safe_out; + } + + /* ok */ + return in; +} + /** Substitute a string for a pattern in another string. Make sure there is enough room! This routine looks for pattern in s and replaces it with insert. It may do multiple replacements or just one. - Any of " ; ' $ or ` in the insert string are replaced with _ + Any of STRING_SUB_UNSAFE_CHARACTERS in the insert string are replaced with _ + if len==0 then the string cannot be extended. This is different from the old use of len==0 which was for no length checks to be done. **/ void string_sub(char *s, const char *pattern, const char *insert, size_t len) { - bool remove_unsafe_characters = true; + const char *unsafe_characters = STRING_SUB_UNSAFE_CHARACTERS; + char safe_character = '_'; char *p; size_t ls, lp, li, i; @@ -76,26 +105,18 @@ void string_sub(char *s, const char *pattern, const char *insert, size_t len) memmove(p+li,p+lp,strlen(p+lp)+1); } for (i=0;i<li;i++) { - switch (insert[i]) { - case '$': - case '`': - case '"': - case '\'': - case ';': - case '%': - case '\r': - case '\n': - if ( remove_unsafe_characters ) { - p[i] = '_'; - /* yes this break should be here - * since we want to fall throw if - * not replacing unsafe chars */ - break; - } - FALL_THROUGH; - default: - p[i] = insert[i]; - } + /* + * Without allow_trailing_dollar we don't + * need to calculate is_last... + */ + const bool is_last = false; + const bool allow_trailing_dollar = false; + + p[i] = mask_unsafe_character(insert[i], + is_last, + allow_trailing_dollar, + unsafe_characters, + safe_character); } s = p + li; ls = ls + li - lp; @@ -157,9 +178,11 @@ char *talloc_string_sub2(TALLOC_CTX *mem_ctx, const char *src, bool replace_once, bool allow_trailing_dollar) { - char *p; - char *s; - char *string; + const char *unsafe_characters = STRING_SUB_UNSAFE_CHARACTERS; + const char safe_character = '_'; + char *p = NULL, + char *s = NULL; + char *string = NULL; ssize_t ls,lp,li,ld, i; if (!insert || !pattern || !*pattern || !src) { @@ -195,36 +218,14 @@ char *talloc_string_sub2(TALLOC_CTX *mem_ctx, const char *src, if (li != lp) { memmove(p+li,p+lp,strlen(p+lp)+1); } - for (i=0; i<li; i++) { - switch (insert[i]) { - case '$': - /* - * allow a trailing $ (as in machine accounts) - */ - if (allow_trailing_dollar && (i == li - 1 )) { - break; - } - - FALL_THROUGH; - case '`': - case '"': - case '\'': - case ';': - case '%': - case '\r': - case '\n': - if (remove_unsafe_characters) { - p[i] = '_'; - continue; - } - - FALL_THROUGH; - default: - /* ok */ - break; - } + for (i=0; i < li; i++) { + bool is_last = (i == li - 1); - p[i] = insert[i]; + p[i] = mask_unsafe_character(insert[i], + is_last, + allow_trailing_dollar, + unsafe_characters, + safe_character); } s = p + li; ls += ld;
lib/util/substitute.h+5 −1 modified@@ -26,14 +26,18 @@ #include <talloc.h> +#define STRING_SUB_UNSAFE_CHARACTERS "$`\"';%\r\n" + /** Substitute a string for a pattern in another string. Make sure there is enough room! This routine looks for pattern in s and replaces it with insert. It may do multiple replacements. - Any of " ; ' $ or ` in the insert string are replaced with _ + Any of STRING_SUB_UNSAFE_CHARACTERS (see above) in the + insert string are replaced with _ + if len==0 then the string cannot be extended. This is different from the old use of len==0 which was for no length checks to be done. **/
23bc9e264e25CVE-2026-4408: s3:samr-server: deny, mask and/or single quote username to 'check password script'
1 file changed · +101 −9
source3/rpc_server/samr/srv_samr_chgpasswd.c+101 −9 modified@@ -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);
Vulnerability mechanics
Root cause
"Missing shell meta-character escaping when substituting the client-controlled username (%u) into the 'check password script' command string."
Attack vector
An unauthenticated remote attacker sends a crafted username containing shell meta-characters (e.g., backticks, semicolons, pipe characters) during a password validation request to a Samba file server or classic domain controller that has the `check password script` option configured with the `%u` substitution character [patch_id=2894986]. The `samba-dcerpcd` service must be started as a system service for the attack path to be reachable. When the script is invoked, the unsanitized username is interpolated into the command string, allowing the attacker to execute arbitrary commands on the server. The CVSS score (Critical) reflects the high complexity due to the non-standard configuration requirement.
Affected code
The vulnerability lies in the `check_password_complexity_internal()` function in `source3/rpc_server/samr/srv_samr_chgpasswd.c` [patch_id=2894986]. The `check password script` smb.conf option, when configured with the `%u` substitution character, passed the client-controlled username directly into a shell command without proper escaping of shell meta-characters. The `talloc_string_sub()` function in `lib/util/substitute.c` performed naive string substitution without sanitizing characters like `` ` ``, `$`, `"`, `'`, `;`, `|`, `&`, `<`, `>` [patch_id=2894971][patch_id=2894977].
What the fix does
The fix introduces `talloc_string_sub_unsafe()` in `lib/util/substitute.c` [patch_id=2894985], which auto-detects quoting context around `%u` and either single-quotes the username, masks unsafe characters with `_`, or falls back to a fixed placeholder `__CVE-2026-4408_FallbackUsername__` when quoting is ambiguous. The `check_password_complexity_internal()` function in `srv_samr_chgpasswd.c` [patch_id=2894986] now first validates the username against `strstr_for_invalid_account_characters()` (rejecting names with characters like `"`, `[`, `]`, `:`, `;`, `|`, `=`, `+`, `*`, `?`, `<`, `>`, `/`, `\`, `,`, control characters, or trailing dots) before performing the safe substitution. Additionally, `STRING_SUB_UNSAFE_CHARACTERS` was expanded to include `|&<>` [patch_id=2894971], control characters are now universally masked via `iscntrl()` [patch_id=2894972], and `testparm` warns administrators about mixed quoting with `%u` [patch_id=2894974]. The documentation was updated to recommend `SAMBA_CPS_ACCOUNT_NAME` over `%u` [patch_id=2894982].
Preconditions
- configThe 'check password script' smb.conf option must be configured with the %u substitution character
- configThe samba-dcerpcd service must be started as a system service
- configThe server must be a Samba file server or classic domain controller (not an AD DC)
- networkNetwork access to the Samba server's RPC endpoint for password validation
Generated on May 28, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3News mentions
0No linked articles in our index yet.