CVE-2026-4480
Description
A flaw was found in the Samba printing subsystem. Samba passes the client-controlled job description string to the command configured with the "print command" setting via the "%J" substitution character without escaping shell meta characters. A remote attacker could exploit this vulnerability by sending a specially crafted print job description that contains unescaped shell characters. This could lead to remote code execution on the affected system.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Samba printing subsystem passes client-controlled job description to print command without escaping shell characters, enabling remote code execution.
Vulnerability
The vulnerability resides in the Samba printing subsystem. Samba passes the client-controlled job description string to the command configured with the print command setting via the %J substitution character without escaping shell metacharacters. This affects Samba versions prior to the security update that addresses this flaw [1][2].
Exploitation
An attacker with network access to the Samba server can submit a specially crafted print job containing unescaped shell metacharacters in the job description. No authentication is explicitly required for submitting print jobs, though the attacker must be able to reach the Samba printing service. The malicious job description is then passed to the print command via %J, allowing shell command injection [1][2].
Impact
Successful exploitation leads to remote code execution on the Samba server with the privileges of the printing subsystem (typically root or the printing user). This gives the attacker full control over the affected system [1][2].
Mitigation
Apply the security update provided by Samba and/or Red Hat as referenced in the advisory. Fixed versions are available from Samba and Red Hat. No workarounds are mentioned in the available references [1][2].
AI Insight generated on May 26, 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
145288e3f8469cCVE-2026-4480: docs-xml/smbdotconf: clarify '%J' in 'print command'
1 file changed · +5 −2
docs-xml/smbdotconf/printing/printcommand.xml+5 −2 modified@@ -21,8 +21,11 @@ <para>%p - the appropriate printer name</para> - <para>%J - the job - name as transmitted by the client.</para> + <para>%J - the job name as transmitted by the client, + but with dangerous characters being replaced by _. + You should use single quotes (directly) around %J, e.g. '%J', + see CVE-2026-4480 for more details. + </para> <para>%c - The number of printed pages of the spooled job (if known).</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",
c6ecded7002cCVE-2026-4480: s3:testparm: warn about 'print command' %J usage
1 file changed · +8 −0
source3/utils/testparm.c+8 −0 modified@@ -953,6 +953,14 @@ static void do_per_share_checks(int s) "parameter is ignored when using CUPS libraries.\n\n", lp_servicename(talloc_tos(), lp_sub, s)); } + if (talloc_string_sub_mixed_quoting(lp_print_command(s), 'J')) { + fprintf(stderr, + "WARNING: Service %s defines a 'print command' " + "with mixed quoting and %%J.\n" + "CVE-2026-4480 changed the way %%J substitution works.\n" + "You should use single quotes (directly) around '%%J'.\n\n", + lp_servicename(talloc_tos(), lp_sub, s)); + } vfs_objects = lp_vfs_objects(s); if (vfs_objects && str_list_check(vfs_objects, "fruit")) {
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; }
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; }
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
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.
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. **/
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; }
b80131fcf582CVE-2026-4480: s3:printing: mask and/or single quote jobname passed as %J to "print command"
1 file changed · +94 −13
source3/printing/print_generic.c+94 −13 modified@@ -19,6 +19,7 @@ #include "includes.h" #include "lib/util/util_file.h" +#include "lib/util/util_str_escape.h" #include "printing.h" #include "smbd/proto.h" #include "source3/lib/substitute.h" @@ -207,6 +208,52 @@ static int generic_queue_get(const char *printer_name, return qcount; } +static const char *replace_print_cmd_J(TALLOC_CTX *mem_ctx, + const char *orig_cmd, + const char *unsafe_jobname, + const char *fallback_jobname) +{ + char *cmd = NULL; + bool modified = false; + bool masked = false; + bool mixed_fallback = false; + + /* + * This replaces unsafe characters with '_'. + * We also mask forward and backslash here. + * + * Then it replaces %J with an single quoted + * version of the masked jobname or it falls + * back to fallback_jobname is the print command + * uses strange mixed quoting. + */ + +#define JOBNAME_UNSAFE_CHARACTERS \ + STRING_SUB_UNSAFE_CHARACTERS "/\\" + + cmd = talloc_string_sub_unsafe(mem_ctx, + orig_cmd, + 'J', + unsafe_jobname, + JOBNAME_UNSAFE_CHARACTERS, + '_', + fallback_jobname, + &modified, + &masked, + &mixed_fallback); + if (cmd == NULL) { + return NULL; + } + + /* + * The caller already checked talloc_string_sub_mixed_quoting() + * and warned the admin, so we don't check mixed_fallback + * here + */ + + return cmd; +} + /**************************************************************************** Submit a file for printing - called from print_job_end() ****************************************************************************/ @@ -222,11 +269,12 @@ static int generic_job_submit(int snum, struct printjob *pjob, char *print_directory = NULL; char *wd = NULL; char *p = NULL; - char *jobname = NULL; + const char *print_cmd = NULL; TALLOC_CTX *ctx = talloc_tos(); fstring job_page_count, job_size; print_queue_struct *q = NULL; print_status_struct status; + const char *jobname = "No Document Name"; /* we print from the directory path to give the best chance of parsing the lpq output */ @@ -255,24 +303,48 @@ static int generic_job_submit(int snum, struct printjob *pjob, return -1; } - jobname = talloc_strdup(ctx, pjob->jobname); - if (!jobname) { - ret = -1; - goto out; + if (pjob->jobname[0] != '\0') { + jobname = pjob->jobname; } - jobname = talloc_string_sub(ctx, jobname, "'", "_"); - if (!jobname) { - ret = -1; - goto out; + + print_cmd = lp_print_command(snum); + if (print_cmd != NULL) { + const char *invalid_jobname = "__CVE-2026-4480_FallbackJobname__"; + + if (talloc_string_sub_mixed_quoting(print_cmd, 'J')) { + /* + * The admin used a strange mixture of + * single and double quotes, fallback + * to InvalidDocumentName and warn about + * it, so that the admin can adjust to + * the use single quotes directly around %J, + * e.g. '%J'. + */ + jobname = invalid_jobname; + D_WARNING("CVE-2026-4480: printer %s " + "strange quoting in 'print command', " + "falling back to jobname=%s, " + "use testparm to fix the configuration\n", + lp_printername(talloc_tos(), lp_sub, snum), + invalid_jobname); + } + + print_cmd = replace_print_cmd_J(ctx, + print_cmd, + jobname, + invalid_jobname); + if (!print_cmd) { + ret = -1; + goto out; + } } fstr_sprintf(job_page_count, "%d", pjob->page_count); fstr_sprintf(job_size, "%zu", pjob->size); /* send it to the system spooler */ ret = print_run_command(snum, lp_printername(talloc_tos(), lp_sub, snum), True, - lp_print_command(snum), NULL, + print_cmd, NULL, "%s", p, - "%J", jobname, "%f", p, "%z", job_size, "%c", job_page_count, @@ -293,17 +365,26 @@ static int generic_job_submit(int snum, struct printjob *pjob, int i; for (i = 0; i < ret; i++) { if (strcmp(q[i].fs_file, p) == 0) { + char *le_jobname = + log_escape(talloc_tos(), jobname); + pjob->sysjob = q[i].sysjob; DEBUG(5, ("new job %u (%s) matches sysjob %d\n", - pjob->jobid, jobname, pjob->sysjob)); + pjob->jobid, le_jobname, pjob->sysjob)); + + TALLOC_FREE(le_jobname); break; } } ret = 0; } if (pjob->sysjob == -1) { + char *le_jobname = log_escape(talloc_tos(), jobname); + DEBUG(2, ("failed to get sysjob for job %u (%s), tracking as " - "Unix job\n", pjob->jobid, jobname)); + "Unix job\n", pjob->jobid, le_jobname)); + + TALLOC_FREE(le_jobname); }
Vulnerability mechanics
Root cause
"Missing escaping of shell meta characters in the client-controlled job description string when substituted via %J into the print command."
Attack vector
A remote attacker who can submit print jobs to a Samba server configured with a non-CUPS printing backend (e.g., LPRng) can send a specially crafted print job description containing shell meta characters such as backticks, semicolons, dollar signs, pipes, or ampersands. When Samba substitutes `%J` in the "print command" template, these characters are passed unescaped to the shell, allowing arbitrary command injection. The attacker must have privileges to submit print jobs (low-privilege requirement per CVSS) and the server must use a "print command" that includes `%J`.
Affected code
The vulnerability resides in the Samba printing subsystem, specifically in `source3/printing/print_generic.c` in the `generic_job_submit()` function. The client-controlled job description string (`pjob->jobname`) was passed to the command configured with the "print command" setting via the `%J` substitution character without escaping shell meta characters. The old code only replaced single quotes (`'`) with underscores, leaving many dangerous characters intact.
What the fix does
The fix in `source3/printing/print_generic.c` [patch_id=2562545] replaces the old naive single-quote stripping with a call to the new `talloc_string_sub_unsafe()` function [patch_id=2562546]. This function first masks all unsafe characters (including `$`, `` ` ``, `"`, `'`, `;`, `%`, `|`, `&`, `
Preconditions
- configSamba must be configured with a non-CUPS printing backend (e.g., LPRng) so that 'print command' is executed
- configThe 'print command' smb.conf setting must include the %J substitution variable
- authAttacker must have privileges to submit print jobs to the Samba server
- networkAttacker can send a crafted job description over the network via the SPOOLSS protocol
- inputAttacker-controlled job name string containing shell meta characters
Generated on May 26, 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.