From: Douglas Bagnall Date: Sat, 9 May 2026 10:02:47 +0000 (+1200) Subject: CVE-2026-4480/CVE-2026-4408: lib/util: add test_string_sub unittests X-Git-Url: http://git.ipfire.org/gitweb/index.cgi?a=commitdiff_plain;h=b6fe311a6ac46bb6cd3af6fdfb5b21c7397069d5;p=thirdparty%2Fsamba.git CVE-2026-4480/CVE-2026-4408: lib/util: add test_string_sub unittests This demonstrates the logic of talloc_string_sub_{mixed_quoting,unsafe}() BUG: https://bugzilla.samba.org/show_bug.cgi?id=16033 BUG: https://bugzilla.samba.org/show_bug.cgi?id=16034 Pair-Programmed-With: Stefan Metzmacher Signed-off-by: Douglas Bagnall Signed-off-by: Stefan Metzmacher --- diff --git a/lib/util/tests/test_string_sub.c b/lib/util/tests/test_string_sub.c new file mode 100644 index 00000000000..da97c1c936c --- /dev/null +++ b/lib/util/tests/test_string_sub.c @@ -0,0 +1,1044 @@ + +#include +#include +#include +#include +#include +#include "replace.h" +#include +#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); +} diff --git a/lib/util/wscript_build b/lib/util/wscript_build index 3e96c6caf5f..9b43d05960b 100644 --- a/lib/util/wscript_build +++ b/lib/util/wscript_build @@ -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) diff --git a/selftest/tests.py b/selftest/tests.py index 0695ac67ea1..0a369e4c47a 100644 --- a/selftest/tests.py +++ b/selftest/tests.py @@ -560,6 +560,8 @@ plantestsuite("samba.unittests.json_logging", "none", [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",