From 47952b62a3582b248932e5ccaf67c8d54d48f529 Mon Sep 17 00:00:00 2001 From: Douglas Bagnall Date: Thu, 26 Mar 2026 14:01:03 +1300 Subject: [PATCH 1/2] CVE-2026-4480: Add a function to escape a single shell word The existing escape_shell_string() function tries to escape an entire command, making it safe from embedded redirection and control operators. It does not escape spaces and tabs though, so the number of string words may not be as expected. For example, printf("ls %s", escape_shell_string("a b c"))) will result in "ls a b c". This function uses strict quoting so all characters are treated as a single word. Thus printf("ls %s", escape_shell_word(mem_ctx, "a b c"))) will result in "ls 'a b c'", which runs ls with a single argument. BUG: https://bugzilla.samba.org/show_bug.cgi?id=16033 Signed-off-by: Douglas Bagnall --- selftest/tests.py | 2 + source3/include/proto.h | 1 + source3/lib/test_util_str.c | 80 +++++++++++++++++++++++++++++++++++++ source3/lib/util_str.c | 43 ++++++++++++++++++++ source3/wscript_build | 5 +++ 5 files changed, 131 insertions(+) create mode 100644 source3/lib/test_util_str.c diff --git a/selftest/tests.py b/selftest/tests.py index 7eace3cbced..c6919661791 100644 --- a/selftest/tests.py +++ b/selftest/tests.py @@ -596,6 +596,8 @@ if ("HAVE_TCP_USER_TIMEOUT" in config_hash): environ={'SOCKET_WRAPPER_DIR': ''}) plantestsuite("samba.unittests.adouble", "none", [os.path.join(bindir(), "test_adouble")]) +plantestsuite("samba.unittests.s3_util_str", "none", + [os.path.join(bindir(), "test_util_str")]) plantestsuite("samba.unittests.gnutls_aead_aes_256_cbc_hmac_sha512", "none", [os.path.join(bindir(), "test_gnutls_aead_aes_256_cbc_hmac_sha512")]) plantestsuite("samba.unittests.gnutls_sp800_108", "none", diff --git a/source3/include/proto.h b/source3/include/proto.h index ca659235697..567c91a26d9 100644 --- a/source3/include/proto.h +++ b/source3/include/proto.h @@ -549,6 +549,7 @@ bool validate_net_name( const char *name, const char *invalid_chars, int max_len); char *escape_shell_string(const char *src); +char *escape_shell_word(TALLOC_CTX *mem_ctx, const char *src); ssize_t full_path_tos(const char *dir, const char *name, char *tmpbuf, size_t tmpbuf_len, char **pdst, char **to_free); diff --git a/source3/lib/test_util_str.c b/source3/lib/test_util_str.c new file mode 100644 index 00000000000..116fdc023b1 --- /dev/null +++ b/source3/lib/test_util_str.c @@ -0,0 +1,80 @@ +/* + * Unix SMB/CIFS implementation. + * + * Copyright (C) 2018 Andreas Schneider + * Copyright (C) 2024 Douglas Bagnall + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "util_str.c" +#include + +#define debug_message(...) do { \ + if (isatty(1)) { \ + print_message(__VA_ARGS__); \ + } \ + } while(0) + + +static void test_escape_shell_word(void **state) +{ + TALLOC_CTX *tmp_ctx = talloc_new(NULL); + size_t i; + struct { + const char *before; + const char *after; + } cases[] = { + { + "hello", + "'hello'" + }, + { + "trailing space ", + "'trailing space '" + }, + { + "quote\"quote'quote'''", + "'quote\"quote'\\''quote'\\'''\\'''\\'''" + }, + { + "", + "''" + }, + { + ">|& '' && \n $foo", + "'>|& '\\'''\\'' && \n $foo'", + }, + }; + + for (i = 0; i < ARRAY_SIZE(cases); i++) { + const char *result = escape_shell_word(tmp_ctx, cases[i].before); + size_t len = strlen(result); + debug_message("expecting «%s» -> «%s», got «%s»\n", + cases[i].before, cases[i].after, result); + assert_int_equal(len, strlen(cases[i].after)); + assert_memory_equal(result, cases[i].after, strlen(cases[i].after)); + } +} + + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_escape_shell_word), + }; + if (!isatty(1)) { + cmocka_set_message_output(CM_OUTPUT_SUBUNIT); + } + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/source3/lib/util_str.c b/source3/lib/util_str.c index 68be9a0ec09..ecb770b0171 100644 --- a/source3/lib/util_str.c +++ b/source3/lib/util_str.c @@ -660,6 +660,49 @@ char *escape_shell_string(const char *src) return ret; } +/* + * Convert a string into a form that will cause it to be treated as a + * single word in shell. We encase everything in single quotes, except + * for single quotes themselves. + * + * For example, + * "won't split on spaces" becomes "'won'\''t split on spaces'". + */ + +char *escape_shell_word(TALLOC_CTX *mem_ctx, const char *src) +{ + size_t srclen = strlen(src); + char *dest_start = talloc_array(mem_ctx, char, (srclen * 4) + 3); + char *dest = dest_start; + if (dest_start == NULL) { + return NULL; + } + + *dest++ = '\''; + + while (*src) { + /* + * If it isn't a single quote, we can copy it. + */ + if (*src != '\'') { + *dest++ = *src++; + continue; + } + /* + * We need to jump out of single-quote mode, add the + * quote, and jump back in. + */ + *dest++ = '\''; + *dest++ = '\\'; + *dest++ = *src++; + *dest++ = '\''; + } + *dest++ = '\''; + *dest++ = '\0'; + return dest_start; +} + + /* * This routine improves performance for operations temporarily acting on a * full path. It is equivalent to the much more expensive diff --git a/source3/wscript_build b/source3/wscript_build index e4a595dd180..ee2af36ee99 100644 --- a/source3/wscript_build +++ b/source3/wscript_build @@ -1121,6 +1121,11 @@ bld.SAMBA3_BINARY('test_adouble', deps='smbd_base STRING_REPLACE cmocka', for_selftest=True) +bld.SAMBA3_BINARY('test_util_str', + source='lib/test_util_str.c', + deps='smbd_base STRING_REPLACE cmocka', + for_selftest=True) + bld.SAMBA3_SUBSYSTEM('STRING_REPLACE', source='lib/string_replace.c') -- 2.43.0 From b11ea60560d7c2f08d5954112fe34a1bfa1948b7 Mon Sep 17 00:00:00 2001 From: Volker Lendecke Date: Sun, 15 Mar 2026 19:15:14 +0100 Subject: [PATCH 2/2] printing: Shell-sanitize jobname passed as %J to "print command" Fix an unauthenticated remote code execution vulnerability with printing set to anything *but* cups and iprint, for example "lprng", so that "print command" is executed upon job submission. If the client-controlled job name is handed to the "print command" via "%J", rpcd_spoolssd passes this to the shell without escaping critical characters. BUG: https://bugzilla.samba.org/show_bug.cgi?id=16033 Signed-off-by: Volker Lendecke Signed-off-by: Douglas Bagnall --- source3/printing/print_generic.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/source3/printing/print_generic.c b/source3/printing/print_generic.c index 855de4ca1f3..380ef0536bc 100644 --- a/source3/printing/print_generic.c +++ b/source3/printing/print_generic.c @@ -254,13 +254,17 @@ static int generic_job_submit(int snum, struct printjob *pjob, if (chdir(print_directory) != 0) { return -1; } + { + char *tmpname = talloc_string_sub(ctx, pjob->jobname, "'", "_"); + if (tmpname == NULL) { + ret = -1; + goto out; + } + jobname = escape_shell_word(ctx, tmpname); - jobname = talloc_strdup(ctx, pjob->jobname); - if (!jobname) { - ret = -1; - goto out; + TALLOC_FREE(tmpname); } - jobname = talloc_string_sub(ctx, jobname, "'", "_"); + if (!jobname) { ret = -1; goto out; -- 2.43.0