Hi, Both CVEs in this report were fixed upstream on 2026-10-31. Neither fix is in a release yet (newest tag is v1.7.2, and both advisories read "through 1.7.2").
CVE-2026-118692 (OS command injection via the custom web-link action, upstream issue #2945) is fixed by commit 0a0638651f31. Instead of fmt::sprintf(template, url) followed by std::system(), the fix parses the template with Glib::shell_parse_argv(), substitutes the URL into individual argv elements, and runs it with Glib::spawn_async() (no shell). The same change also covers the file-link and folder-link custom actions, which share the std::system sink. CVE-2026-118693 (arbitrary local file read via a crafted embedded-file name, upstream issue #2944) is fixed by commit 61fca359a431, which validates the embedded-file "filename" attribute instead of trusting it. Affected shipped versions (verified in the source): bookworm 0.99.48 and trixie/sid/forky 1.2.0 both carry the vulnerable code (link_clicked sprintf+system; open_filepath/open_folderpath sprintf+system; and _create_image_from_xml reading "filename"). Attached are backports of the CVE-2026-118692 fix, adapted to the shipped versions (the upstream commit does not cherry-pick: it was refactored into ct_filesystem.cc, while the shipped versions have the call inline): CVE-2026-118692-cherrytree-1.2.0.patch trixie / sid / forky (1.2.0+dfsg-1) CVE-2026-118692-cherrytree-0.99.48.patch bookworm (0.99.48+dfsg-1) Both are DEP-3 and touch three files (ct_actions_others.cc, ct_filesystem.cc, ct_filesystem.h). They route all three custom-action sinks (web, file, folder link) through a new helper that parses the trusted template with Glib::shell_parse_argv, substitutes the untrusted argument into argv elements, and spawns with Glib::spawn_async -- no shell. The existing default templates keep working: the helper drops the historical trailing "&". ct_config.h is left unchanged. Verification: - Both patches apply cleanly to the packaged sources (patch -p1). - I unit-tested the ported helper in isolation against the real g_shell_parse_argv: an argument containing $(...), backticks or ';' stays a single argv element for every template shape tested; and on a live payload, the old std::system path runs the injected command while the new argv+spawn path does not. - I did not rebuild the full GTK application; the change uses only Glib/spdlog calls already present in these files. I can provide a backport for CVE-2026-118693 as well if that would help. Regards, Sumit
Description: Prevent OS command injection via custom link actions (CVE-2026-118692) CtActions::link_clicked (web link custom action) and fs::open_filepath / fs::open_folderpath (file / folder link custom actions) built a shell command with fmt::sprintf(customAct, arg) and ran it through std::system(), so a web-link URL or a file/folder path containing $(...), backticks or ';' was interpreted by the shell. When the non-default Custom Link Action option is enabled, a crafted document could thus run arbitrary commands on click. . Backport of upstream commit 0a0638651f31 (not yet in a release). The trusted command template is parsed with Glib::shell_parse_argv(), the untrusted argument is substituted into individual argv elements (or appended when the template has no %s), and the program is run with Glib::spawn_async() without a shell, so shell metacharacters in the argument are never interpreted. The trailing "&" historically used in the default templates for backgrounding is dropped (spawn_async is already asynchronous), so the existing default templates keep working unchanged. Author: Sumit Verma <[email protected]> Origin: backport, https://github.com/giuspen/cherrytree/commit/0a0638651f31 Bug-Debian: https://bugs.debian.org/1152740 Forwarded: not-needed (fixed upstream) Last-Update: 2026-11-06 --- --- a/src/ct/ct_actions_others.cc +++ b/src/ct/ct_actions_others.cc @@ -333,13 +333,7 @@ if (link_entry.type == CtConst::LINK_TYPE_WEBS) { // link to webpage Glib::ustring clean_weblink = str::replace(link_entry.webs, "amp;", ""); if (_pCtConfig->weblinkCustomOn) { - std::string cmd = fmt::sprintf(_pCtConfig->weblinkCustomAct, clean_weblink.raw()); - int retr = std::system(cmd.c_str()); - if (retr == -1) { - // Internal std::system error - spdlog::error("Error while executing: '{}'; Message: {}", cmd, std::strerror(errno)); - return; - } + fs::open_custom_action(_pCtConfig->weblinkCustomAct, clean_weblink.raw()); } else { fs::open_weblink(clean_weblink); --- a/src/ct/ct_filesystem.cc +++ b/src/ct/ct_filesystem.cc @@ -22,6 +22,8 @@ */ #include <glibmm/miscutils.h> +#include <glibmm/shell.h> +#include <glibmm/spawn.h> #include <giomm/file.h> #include <glib/gstdio.h> #include <curl/curl.h> @@ -315,18 +317,76 @@ _locale_env_vars_set_for_external_cmd(false/*isPre*/); } +static std::vector<std::string> custom_action_build_argv(const std::string& template_cmd, const std::string& target_arg) +{ + if (template_cmd.empty()) { + return {}; + } + std::vector<std::string> argv; + try { + argv = Glib::shell_parse_argv(template_cmd); + } + catch (const Glib::Error& error) { + spdlog::error("custom_action_build_argv: failed to parse template '{}': {}", template_cmd, error.what().raw()); + return {}; + } + if (argv.empty()) { + return {}; + } + + // Strip trailing "&" historically used for shell backgrounding + if (argv.back() == "&") { + argv.pop_back(); + } + if (argv.empty()) { + return {}; + } + + bool substituted = false; + for (auto& arg : argv) { + size_t pos = 0; + while ((pos = arg.find("%s", pos)) != std::string::npos) { + arg.replace(pos, 2, target_arg); + pos += target_arg.length(); + substituted = true; + } + } + if (!substituted) { + argv.push_back(target_arg); + } + return argv; +} + +bool open_custom_action(const std::string& template_cmd, const std::string& target_arg) +{ + const std::vector<std::string> argv = custom_action_build_argv(template_cmd, target_arg); + if (argv.empty()) { + return false; + } + + _locale_env_vars_set_for_external_cmd(true/*isPre*/); + bool spawned = false; + try { +#if GTKMM_MAJOR_VERSION >= 4 + Glib::spawn_async("", argv, Glib::SpawnFlags::SEARCH_PATH); +#else + Glib::spawn_async("", argv, Glib::SpawnFlags::SPAWN_SEARCH_PATH); +#endif + spawned = true; + } + catch (const Glib::Error& error) { + spdlog::error("open_custom_action: failed to spawn '{}': {}", argv.front(), error.what().raw()); + } + _locale_env_vars_set_for_external_cmd(false/*isPre*/); + return spawned; +} + // Open Filepath with External App void open_filepath(const fs::path& filepath, bool open_folder_if_file_not_exists, CtConfig* config) { spdlog::debug("fs::open_filepath {}", filepath.string()); if (config->filelinkCustomOn) { - std::string cmd = fmt::sprintf(config->filelinkCustomAct, filepath.string()); - _locale_env_vars_set_for_external_cmd(true/*isPre*/); - const int retVal = std::system(cmd.c_str()); - _locale_env_vars_set_for_external_cmd(false/*isPre*/); - if (retVal != 0) { - spdlog::error("system({}) returned {}", cmd, retVal); - } + open_custom_action(config->filelinkCustomAct, filepath.string()); } else { if (open_folder_if_file_not_exists && !fs::exists(filepath)) { @@ -347,13 +407,7 @@ { spdlog::debug("fs::open_folderpath {}", folderpath.string()); if (config->folderlinkCustomOn) { - std::string cmd = fmt::sprintf(config->folderlinkCustomAct, folderpath.string()); - _locale_env_vars_set_for_external_cmd(true/*isPre*/); - const int retVal = std::system(cmd.c_str()); - _locale_env_vars_set_for_external_cmd(false/*isPre*/); - if (retVal != 0) { - spdlog::error("system({}) returned {}", cmd, retVal); - } + open_custom_action(config->folderlinkCustomAct, folderpath.string()); } else { _open_path_with_default_app(folderpath); --- a/src/ct/ct_filesystem.h +++ b/src/ct/ct_filesystem.h @@ -82,6 +82,7 @@ void open_weblink(const std::string& link); void open_filepath(const path& filepath, bool open_folder_if_file_not_exists, CtConfig* config); void open_folderpath(const path& folderpath, CtConfig* config); +bool open_custom_action(const std::string& template_cmd, const std::string& target_arg); path prepare_export_folder(const path& dir_place, path new_folder, bool overwrite_existing);
Description: Prevent OS command injection via custom link actions (CVE-2026-118692) CtActions::link_clicked (web link custom action) and fs::open_filepath / fs::open_folderpath (file / folder link custom actions) built a shell command with fmt::sprintf(customAct, arg) and ran it through std::system(), so a web-link URL or a file/folder path containing $(...), backticks or ';' was interpreted by the shell. When the non-default Custom Link Action option is enabled, a crafted document could thus run arbitrary commands on click. . Backport of upstream commit 0a0638651f31 (not yet in a release). The trusted command template is parsed with Glib::shell_parse_argv(), the untrusted argument is substituted into individual argv elements (or appended when the template has no %s), and the program is run with Glib::spawn_async() without a shell, so shell metacharacters in the argument are never interpreted. The trailing "&" historically used in the default templates for backgrounding is dropped (spawn_async is already asynchronous), so the existing default templates keep working unchanged. Author: Sumit Verma <[email protected]> Origin: backport, https://github.com/giuspen/cherrytree/commit/0a0638651f31 Bug-Debian: https://bugs.debian.org/1152740 Forwarded: not-needed (fixed upstream) Last-Update: 2026-11-06 --- --- a/src/ct/ct_actions_others.cc +++ b/src/ct/ct_actions_others.cc @@ -344,13 +344,7 @@ if (link_entry.type == CtConst::LINK_TYPE_WEBS) { // link to webpage Glib::ustring clean_weblink = str::replace(link_entry.webs, "amp;", ""); if (_pCtConfig->weblinkCustomOn) { - std::string cmd = fmt::sprintf(_pCtConfig->weblinkCustomAct, clean_weblink.raw()); - int retr = std::system(cmd.c_str()); - if (retr == -1) { - // Internal std::system error - spdlog::error("Error while executing: '{}'; Message: {}", cmd, std::strerror(errno)); - return; - } + fs::open_custom_action(_pCtConfig->weblinkCustomAct, clean_weblink.raw()); } else { fs::open_weblink(clean_weblink); --- a/src/ct/ct_filesystem.cc +++ b/src/ct/ct_filesystem.cc @@ -22,6 +22,8 @@ */ #include <glibmm/miscutils.h> +#include <glibmm/shell.h> +#include <glibmm/spawn.h> #include <glib/gstdio.h> #include <curl/curl.h> #include <system_error> @@ -272,18 +274,76 @@ _locale_env_vars_set_for_external_cmd(false/*isPre*/); } +static std::vector<std::string> custom_action_build_argv(const std::string& template_cmd, const std::string& target_arg) +{ + if (template_cmd.empty()) { + return {}; + } + std::vector<std::string> argv; + try { + argv = Glib::shell_parse_argv(template_cmd); + } + catch (const Glib::Error& error) { + spdlog::error("custom_action_build_argv: failed to parse template '{}': {}", template_cmd, error.what().raw()); + return {}; + } + if (argv.empty()) { + return {}; + } + + // Strip trailing "&" historically used for shell backgrounding + if (argv.back() == "&") { + argv.pop_back(); + } + if (argv.empty()) { + return {}; + } + + bool substituted = false; + for (auto& arg : argv) { + size_t pos = 0; + while ((pos = arg.find("%s", pos)) != std::string::npos) { + arg.replace(pos, 2, target_arg); + pos += target_arg.length(); + substituted = true; + } + } + if (!substituted) { + argv.push_back(target_arg); + } + return argv; +} + +bool open_custom_action(const std::string& template_cmd, const std::string& target_arg) +{ + const std::vector<std::string> argv = custom_action_build_argv(template_cmd, target_arg); + if (argv.empty()) { + return false; + } + + _locale_env_vars_set_for_external_cmd(true/*isPre*/); + bool spawned = false; + try { +#if GTKMM_MAJOR_VERSION >= 4 + Glib::spawn_async("", argv, Glib::SpawnFlags::SEARCH_PATH); +#else + Glib::spawn_async("", argv, Glib::SpawnFlags::SPAWN_SEARCH_PATH); +#endif + spawned = true; + } + catch (const Glib::Error& error) { + spdlog::error("open_custom_action: failed to spawn '{}': {}", argv.front(), error.what().raw()); + } + _locale_env_vars_set_for_external_cmd(false/*isPre*/); + return spawned; +} + // Open Filepath with External App void open_filepath(const fs::path& filepath, bool open_folder_if_file_not_exists, CtConfig* config) { spdlog::debug("fs::open_filepath {}", filepath); if (config->filelinkCustomOn) { - std::string cmd = fmt::sprintf(config->filelinkCustomAct, filepath.string()); - _locale_env_vars_set_for_external_cmd(true/*isPre*/); - const int retVal = std::system(cmd.c_str()); - _locale_env_vars_set_for_external_cmd(false/*isPre*/); - if (retVal != 0) { - spdlog::error("system({}) returned {}", cmd, retVal); - } + open_custom_action(config->filelinkCustomAct, filepath.string()); } else { if (open_folder_if_file_not_exists && !fs::exists(filepath)) { @@ -304,13 +364,7 @@ { spdlog::debug("fs::open_folderpath {}", folderpath); if (config->folderlinkCustomOn) { - std::string cmd = fmt::sprintf(config->folderlinkCustomAct, folderpath.string()); - _locale_env_vars_set_for_external_cmd(true/*isPre*/); - const int retVal = std::system(cmd.c_str()); - _locale_env_vars_set_for_external_cmd(false/*isPre*/); - if (retVal != 0) { - spdlog::error("system({}) returned {}", cmd, retVal); - } + open_custom_action(config->folderlinkCustomAct, folderpath.string()); } else { _open_path_with_default_app(folderpath); --- a/src/ct/ct_filesystem.h +++ b/src/ct/ct_filesystem.h @@ -81,6 +81,7 @@ void open_weblink(const std::string& link); void open_filepath(const path& filepath, bool open_folder_if_file_not_exists, CtConfig* config); void open_folderpath(const path& folderpath, CtConfig* config); +bool open_custom_action(const std::string& template_cmd, const std::string& target_arg); path prepare_export_folder(const path& dir_place, path new_folder, bool overwrite_existing);

