security: harden untrusted-input handling (issue #148)
All checks were successful
CI Code / Check spelling (pull_request) Successful in 18s
CI Code / Check coding style (pull_request) Successful in 47s
CI Code / Linux (debian) (pull_request) Successful in 9m33s
CI Code / Linux (ubuntu) (pull_request) Successful in 9m41s
CI Code / Code Coverage (pull_request) Successful in 8m3s
CI Code / Linux (arch) (pull_request) Successful in 11m0s

T02: guard the receive-path handlers that dereferenced jid_create()
without a NULL check — MUC join errors, subscribed/unsubscribed
presence and, with silence.non-roster enabled, every incoming message.
A stanza with a missing or malformed 'from' crashed the client. The
XEP-0280 carbon path carried the same class twice: the forwarded
message's 'from' was handed to xmpp_jid_bare(), which dereferences the
jid it is given, and a malformed 'to' left the carbon dispatch
dereferencing a NULL jid_create() result (REQ-INP-01)

T11: restrict /url open and /url save to http, https and aesgcm, so a
received file:, javascript: or data: URL is refused (REQ-INP-06); spawn
terminal-notifier through g_spawn_async with an argv instead of
building a shell command for system() (REQ-INP-07); apply the XEP-0359
disco gate to MAM result ids, as live stanza-ids already do
(REQ-INP-05); replace control and bidi-reordering characters in
incoming message bodies with U+FFFD before they reach the terminal, the
logs and the database, keeping LRM/RLM for legitimate RTL text. That
pass now runs on every display path: the OX one, where the call had
been left commented out since the feature landed, and outgoing carbons,
whose body is forwarded by the server (REQ-INP-08); cover JID
part-length boundaries and invalid UTF-8 (REQ-INP-02)

T10: replace strcpy/strcat/alloca and sprintf with g_strdup_printf and
g_snprintf (REQ-MEM-03); allocate the OMEMO key buffers with g_malloc
so a failed allocation cannot reach the following memcpy (REQ-MEM-04);
remove the variable-length arrays and enforce -Werror=vla. Two of them
were sized from remote input: the disco#info feature count and a chat
message word length. The flag also caught a one-past-the-end write and
a leak in the plugin autocompleter bindings (REQ-MEM-09). The OX
receive path leaked every decrypted body, dropping the pointer instead
of freeing it, and a failed strdup no longer costs the message body
This commit is contained in:
2026-07-30 12:27:37 +03:00
parent d914e42ff6
commit afd9d84723
32 changed files with 554 additions and 111 deletions

View File

@@ -493,7 +493,7 @@ _inp_edited(const wint_t ch)
}
// printable
char bytes[MB_CUR_MAX + 1];
char bytes[PROF_MB_CUR_MAX + 1]; // compile-time bound, the locale value is checked at startup
size_t utf_len = wcrtomb(bytes, ch, &mbstate);
if (utf_len == (size_t)-1) {
return 0;

View File

@@ -454,22 +454,10 @@ _mucwin_print_triggers(ProfWin* window, const char* const message, GList* trigge
win_appendln_highlight(window, THEME_ROOMTRIGGER, "%s", message);
} else {
if (first_trigger_pos > 0) {
char message_section[strlen(message) + 1];
int i = 0;
while (i < first_trigger_pos) {
message_section[i] = message[i];
i++;
}
message_section[i] = '\0';
auto_gchar gchar* message_section = g_strndup(message, (gsize)first_trigger_pos);
win_append_highlight(window, THEME_ROOMTRIGGER, "%s", message_section);
}
char trigger_section[first_trigger_len + 1];
int i = 0;
while (i < first_trigger_len) {
trigger_section[i] = message[first_trigger_pos + i];
i++;
}
trigger_section[i] = '\0';
auto_gchar gchar* trigger_section = g_strndup(&message[first_trigger_pos], (gsize)first_trigger_len);
if (first_trigger_pos + first_trigger_len < (int)strlen(message)) {
win_append_highlight(window, THEME_ROOMTRIGGER_TERM, "%s", trigger_section);

View File

@@ -22,6 +22,7 @@
#include <windows.h>
#endif
#include "common.h"
#include "log.h"
#include "config/preferences.h"
#include "ui/ui.h"
@@ -122,47 +123,42 @@ _notify(const char* const message, int timeout, const char* const category)
static void
_notify(const char* const message, int timeout, const char* const category)
{
GString* notify_command = g_string_new("terminal-notifier -title \"Profanity\" -message '");
if (message == NULL) {
return;
}
auto_char char* escaped_single = str_replace(message, "'", "'\\''");
if (escaped_single[0] == '<') {
g_string_append(notify_command, "\\<");
g_string_append(notify_command, &escaped_single[1]);
} else if (escaped_single[0] == '[') {
g_string_append(notify_command, "\\[");
g_string_append(notify_command, &escaped_single[1]);
} else if (escaped_single[0] == '(') {
g_string_append(notify_command, "\\(");
g_string_append(notify_command, &escaped_single[1]);
} else if (escaped_single[0] == '{') {
g_string_append(notify_command, "\\{");
g_string_append(notify_command, &escaped_single[1]);
// terminal-notifier swallows a leading <, [, ( or { — escape it for its parser
auto_gchar gchar* msg = NULL;
if (message[0] == '<' || message[0] == '[' || message[0] == '(' || message[0] == '{') {
msg = g_strdup_printf("\\%s", message);
} else {
g_string_append(notify_command, escaped_single);
msg = g_strdup(message);
}
g_string_append(notify_command, "'");
const gchar* argv[16]; // headroom: an added flag must not silently overrun
guint i = 0;
argv[i++] = "terminal-notifier";
argv[i++] = "-title";
argv[i++] = "Profanity";
argv[i++] = "-message";
argv[i++] = msg;
char* term_name = getenv("TERM_PROGRAM");
char* app_id = NULL;
const char* term_name = getenv("TERM_PROGRAM");
if (g_strcmp0(term_name, "Apple_Terminal") == 0) {
app_id = "com.apple.Terminal";
argv[i++] = "-sender";
argv[i++] = "com.apple.Terminal";
} else if (g_strcmp0(term_name, "iTerm.app") == 0) {
app_id = "com.googlecode.iterm2";
argv[i++] = "-sender";
argv[i++] = "com.googlecode.iterm2";
}
argv[i] = NULL;
if (app_id) {
g_string_append(notify_command, " -sender ");
g_string_append(notify_command, app_id);
GError* err = NULL;
// argv-based spawn: the message is one argument, the shell never sees it
if (!g_spawn_async(NULL, (gchar**)argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, &err)) {
log_error("Could not send desktop notification: %s", err->message);
g_error_free(err);
}
int res = system(notify_command->str);
if (res == -1) {
log_error("Could not send desktop notification.");
}
g_string_free(notify_command, TRUE);
}
#else
static void

View File

@@ -32,6 +32,7 @@
#include <curses.h>
#endif
#include "common.h"
#include "log.h"
#include "config/theme.h"
#include "config/preferences.h"
@@ -2115,7 +2116,7 @@ _win_print_wrapped(WINDOW* win, const char* const message, int indent, int pad_i
_win_indent(win, indent + pad_indent);
}
gchar copy[wordi + 1];
gchar copy[PROF_MB_CUR_MAX + 1]; // one UTF-8 character, not the whole word
g_utf8_strncpy(copy, word_ch, 1);
waddstr(win, copy);