From 2d81eded1e64737d2ecca278efc2a84be7dbd8f5 Mon Sep 17 00:00:00 2001 From: Vsevolod Stakhov Date: Fri, 14 Sep 2012 20:59:23 +0400 Subject: [PATCH] * Initial approach to RESTful controller. Fix security issues in fstring handling. --- src/controller.c | 305 +++++++++++++++++++++++++++++++++++++---------- src/fstring.c | 3 +- src/main.h | 13 +- src/protocol.c | 7 +- src/protocol.h | 8 ++ src/smtp.c | 27 +++-- src/smtp_proto.c | 1 + 7 files changed, 284 insertions(+), 80 deletions(-) diff --git a/src/controller.c b/src/controller.c index c5aebb699..7bd90e7db 100644 --- a/src/controller.c +++ b/src/controller.c @@ -70,7 +70,9 @@ enum command_type { COMMAND_HELP, COMMAND_COUNTERS, COMMAND_SYNC, - COMMAND_WEIGHTS + COMMAND_WEIGHTS, + COMMAND_GET, + COMMAND_POST }; struct controller_command { @@ -106,7 +108,9 @@ static struct controller_command commands[] = { {"counters", FALSE, COMMAND_COUNTERS}, {"sync", FALSE, COMMAND_SYNC}, {"learn_spam", TRUE, COMMAND_LEARN_SPAM}, - {"learn_ham", TRUE, COMMAND_LEARN_HAM} + {"learn_ham", TRUE, COMMAND_LEARN_HAM}, + {"get", FALSE, COMMAND_GET}, + {"post", FALSE, COMMAND_POST} }; static GList *custom_commands = NULL; @@ -190,6 +194,10 @@ free_session (void *ud) } rspamd_remove_dispatcher (session->dispatcher); + if (session->kwargs) { + g_hash_table_destroy (session->kwargs); + } + close (session->sock); memory_pool_delete (session->session_pool); @@ -467,6 +475,12 @@ process_command (struct controller_command *cmd, gchar **cmd_args, struct contro struct rspamd_controller_ctx *ctx = session->worker->ctx; switch (cmd->type) { + case COMMAND_GET: + case COMMAND_POST: + session->restful = TRUE; + session->state = STATE_HEADER; + session->kwargs = g_hash_table_new (rspamd_strcase_hash, rspamd_strcase_equal); + break; case COMMAND_PASSWORD: arg = *cmd_args; if (!arg || *arg == '\0') { @@ -553,34 +567,65 @@ process_command (struct controller_command *cmd, gchar **cmd_args, struct contro break; case COMMAND_LEARN_SPAM: if (check_auth (cmd, session)) { - arg = *cmd_args; - if (!arg || *arg == '\0') { - msg_debug ("no statfile specified in learn command"); - r = rspamd_snprintf (out_buf, sizeof (out_buf), "learn command requires at least two arguments: stat filename and its size" CRLF); - if (! rspamd_dispatcher_write (session->dispatcher, out_buf, r, FALSE, FALSE)) { - return FALSE; + if (!session->restful) { + arg = *cmd_args; + if (!arg || *arg == '\0') { + msg_debug ("no statfile specified in learn command"); + r = rspamd_snprintf (out_buf, sizeof (out_buf), "learn_spam command requires at least two arguments: classifier name and a message's size" CRLF); + if (! rspamd_dispatcher_write (session->dispatcher, out_buf, r, FALSE, FALSE)) { + return FALSE; + } + return TRUE; } - return TRUE; - } - arg = *(cmd_args + 1); - if (arg == NULL || *arg == '\0') { - msg_debug ("no message size specified in learn command"); - r = rspamd_snprintf (out_buf, sizeof (out_buf), "learn command requires at least two arguments: symbol and message size" CRLF); - if (! rspamd_dispatcher_write (session->dispatcher, out_buf, r, FALSE, FALSE)) { - return FALSE; + arg = *(cmd_args + 1); + if (arg == NULL || *arg == '\0') { + msg_debug ("no message size specified in learn command"); + r = rspamd_snprintf (out_buf, sizeof (out_buf), "learn_spam command requires at least two arguments: classifier name and a message's size" CRLF); + if (! rspamd_dispatcher_write (session->dispatcher, out_buf, r, FALSE, FALSE)) { + return FALSE; + } + return TRUE; } - return TRUE; + size = strtoul (arg, &err_str, 10); + if (err_str && *err_str != '\0') { + msg_debug ("message size is invalid: %s", arg); + r = rspamd_snprintf (out_buf, sizeof (out_buf), "learn size is invalid" CRLF); + if (! rspamd_dispatcher_write (session->dispatcher, out_buf, r, FALSE, FALSE)) { + return FALSE; + } + return TRUE; + } + cl = find_classifier_conf (session->cfg, *cmd_args); } - size = strtoul (arg, &err_str, 10); - if (err_str && *err_str != '\0') { - msg_debug ("message size is invalid: %s", arg); - r = rspamd_snprintf (out_buf, sizeof (out_buf), "learn size is invalid" CRLF); - if (! rspamd_dispatcher_write (session->dispatcher, out_buf, r, FALSE, FALSE)) { - return FALSE; + else { + if ((arg = g_hash_table_lookup (session->kwargs, "classifier")) == NULL) { + msg_debug ("no classifier specified in learn command"); + r = rspamd_snprintf (out_buf, sizeof (out_buf), "learn_spam command requires at least two arguments: classifier name and a message's size" CRLF); + if (! rspamd_dispatcher_write (session->dispatcher, out_buf, r, FALSE, FALSE)) { + return FALSE; + } + return TRUE; + } + else { + cl = find_classifier_conf (session->cfg, arg); + } + if ((arg = g_hash_table_lookup (session->kwargs, "content-length")) == NULL) { + msg_debug ("no size specified in learn command"); + r = rspamd_snprintf (out_buf, sizeof (out_buf), "learn_spam command requires at least two arguments: classifier name and a message's size" CRLF); + return rspamd_dispatcher_write (session->dispatcher, out_buf, r, FALSE, FALSE); + } + else { + size = strtoul (arg, &err_str, 10); + if (err_str && *err_str != '\0') { + msg_debug ("message size is invalid: %s", arg); + r = rspamd_snprintf (out_buf, sizeof (out_buf), "learn size is invalid" CRLF); + if (! rspamd_dispatcher_write (session->dispatcher, out_buf, r, FALSE, FALSE)) { + return FALSE; + } + return TRUE; + } } - return TRUE; } - cl = find_classifier_conf (session->cfg, *cmd_args); session->learn_classifier = cl; @@ -592,34 +637,65 @@ process_command (struct controller_command *cmd, gchar **cmd_args, struct contro break; case COMMAND_LEARN_HAM: if (check_auth (cmd, session)) { - arg = *cmd_args; - if (!arg || *arg == '\0') { - msg_debug ("no statfile specified in learn command"); - r = rspamd_snprintf (out_buf, sizeof (out_buf), "learn command requires at least two arguments: stat filename and its size" CRLF); - if (! rspamd_dispatcher_write (session->dispatcher, out_buf, r, FALSE, FALSE)) { - return FALSE; + if (!session->restful) { + arg = *cmd_args; + if (!arg || *arg == '\0') { + msg_debug ("no statfile specified in learn command"); + r = rspamd_snprintf (out_buf, sizeof (out_buf), "learn_spam command requires at least two arguments: classifier name and a message's size" CRLF); + if (! rspamd_dispatcher_write (session->dispatcher, out_buf, r, FALSE, FALSE)) { + return FALSE; + } + return TRUE; } - return TRUE; - } - arg = *(cmd_args + 1); - if (arg == NULL || *arg == '\0') { - msg_debug ("no message size specified in learn command"); - r = rspamd_snprintf (out_buf, sizeof (out_buf), "learn command requires at least two arguments: symbol and message size" CRLF); - if (! rspamd_dispatcher_write (session->dispatcher, out_buf, r, FALSE, FALSE)) { - return FALSE; + arg = *(cmd_args + 1); + if (arg == NULL || *arg == '\0') { + msg_debug ("no message size specified in learn command"); + r = rspamd_snprintf (out_buf, sizeof (out_buf), "learn_spam command requires at least two arguments: classifier name and a message's size" CRLF); + if (! rspamd_dispatcher_write (session->dispatcher, out_buf, r, FALSE, FALSE)) { + return FALSE; + } + return TRUE; } - return TRUE; + size = strtoul (arg, &err_str, 10); + if (err_str && *err_str != '\0') { + msg_debug ("message size is invalid: %s", arg); + r = rspamd_snprintf (out_buf, sizeof (out_buf), "learn size is invalid" CRLF); + if (! rspamd_dispatcher_write (session->dispatcher, out_buf, r, FALSE, FALSE)) { + return FALSE; + } + return TRUE; + } + cl = find_classifier_conf (session->cfg, *cmd_args); } - size = strtoul (arg, &err_str, 10); - if (err_str && *err_str != '\0') { - msg_debug ("message size is invalid: %s", arg); - r = rspamd_snprintf (out_buf, sizeof (out_buf), "learn size is invalid" CRLF); - if (! rspamd_dispatcher_write (session->dispatcher, out_buf, r, FALSE, FALSE)) { - return FALSE; + else { + if ((arg = g_hash_table_lookup (session->kwargs, "classifier")) == NULL) { + msg_debug ("no classifier specified in learn command"); + r = rspamd_snprintf (out_buf, sizeof (out_buf), "learn_spam command requires at least two arguments: classifier name and a message's size" CRLF); + if (! rspamd_dispatcher_write (session->dispatcher, out_buf, r, FALSE, FALSE)) { + return FALSE; + } + return TRUE; + } + else { + cl = find_classifier_conf (session->cfg, arg); + } + if ((arg = g_hash_table_lookup (session->kwargs, "content-length")) == NULL) { + msg_debug ("no size specified in learn command"); + r = rspamd_snprintf (out_buf, sizeof (out_buf), "learn_spam command requires at least two arguments: classifier name and a message's size" CRLF); + return rspamd_dispatcher_write (session->dispatcher, out_buf, r, FALSE, FALSE); + } + else { + size = strtoul (arg, &err_str, 10); + if (err_str && *err_str != '\0') { + msg_debug ("message size is invalid: %s", arg); + r = rspamd_snprintf (out_buf, sizeof (out_buf), "learn size is invalid" CRLF); + if (! rspamd_dispatcher_write (session->dispatcher, out_buf, r, FALSE, FALSE)) { + return FALSE; + } + return TRUE; + } } - return TRUE; } - cl = find_classifier_conf (session->cfg, *cmd_args); session->learn_classifier = cl; @@ -631,6 +707,7 @@ process_command (struct controller_command *cmd, gchar **cmd_args, struct contro break; case COMMAND_LEARN: if (check_auth (cmd, session)) { + /* TODO: remove this command as currenly it should not be used anywhere */ arg = *cmd_args; if (!arg || *arg == '\0') { msg_debug ("no statfile specified in learn command"); @@ -722,6 +799,7 @@ process_command (struct controller_command *cmd, gchar **cmd_args, struct contro break; case COMMAND_WEIGHTS: + /* TODO: remove this command as currenly it should not be used anywhere */ arg = *cmd_args; if (!arg || *arg == '\0') { msg_debug ("no statfile specified in weights command"); @@ -798,35 +876,39 @@ process_command (struct controller_command *cmd, gchar **cmd_args, struct contro return TRUE; } -static gboolean -process_custom_command (gchar *line, gchar **cmd_args, struct controller_session *session) +static controller_func_t +parse_custom_command (gchar *line, gchar **cmd_args, struct controller_session *session, gsize len) { GList *cur; struct custom_controller_command *cmd; + if (len == 0) { + len = strlen (line); + } cur = custom_commands; while (cur) { cmd = cur->data; - if (g_ascii_strcasecmp (cmd->command, line) == 0) { - /* Call handler */ - cmd->handler (cmd_args, session); - return TRUE; + if (g_ascii_strncasecmp (cmd->command, line, len) == 0) { + return cmd->handler; } cur = g_list_next (cur); } - return FALSE; + return NULL; } static struct controller_command * -process_normal_command (const gchar *line) +parse_normal_command (const gchar *line, gsize len) { guint i; struct controller_command *c; + if (len == 0) { + len = strlen (line); + } for (i = 0; i < G_N_ELEMENTS (commands); i ++) { c = &commands[i]; - if (g_ascii_strcasecmp (line, c->command) == 0) { + if (g_ascii_strncasecmp (line, c->command, len) == 0) { return c; } } @@ -834,6 +916,58 @@ process_normal_command (const gchar *line) return NULL; } +static gboolean +process_header (f_str_t *line, struct controller_session *session) +{ + gchar *headern; + struct controller_command *command; + struct rspamd_controller_ctx *ctx = session->worker->ctx; + controller_func_t custom_handler; + + headern = separate_command (line, ':'); + + if (line == NULL || headern == NULL) { + return FALSE; + } + /* Eat whitespaces */ + g_strstrip (headern); + fstrstrip (line); + + if (*headern == 'c' || *headern == 'C') { + if (g_ascii_strcasecmp (headern, "command") == 0) { + /* This header is actually command */ + command = parse_normal_command (line->begin, line->len); + if (command == NULL) { + if ((custom_handler = parse_custom_command (line->begin, NULL, session, line->len)) == NULL) { + msg_info ("bad command header: %V", line); + return FALSE; + } + else { + session->custom_handler = custom_handler; + } + } + session->cmd = command; + return TRUE; + } + } + else if (*headern == 'p' || *headern == 'P') { + /* Password header */ + if (g_ascii_strcasecmp (headern, "password") == 0) { + if (line->len == strlen (ctx->password) && memcmp (line->begin, ctx->password, line->len) == 0) { + session->authorized = TRUE; + } + else { + msg_info ("wrong password in controller command"); + } + return TRUE; + } + } + + g_hash_table_insert (session->kwargs, headern, fstrcstr (line, session->session_pool)); + + return TRUE; +} + /* * Called if all filters are processed, non-threaded and simple version */ @@ -889,6 +1023,7 @@ controller_read_socket (f_str_t * in, void *arg) GTree *tokens = NULL; GError *err = NULL; f_str_t c; + controller_func_t custom_handler; switch (session->state) { case STATE_COMMAND: @@ -901,24 +1036,27 @@ controller_read_socket (f_str_t * in, void *arg) if (len > 0) { cmd = g_strstrip (params[0]); - command = process_normal_command (cmd); + command = parse_normal_command (cmd, 0); if (command != NULL) { if (! process_command (command, ¶ms[1], session)) { return FALSE; } } else { - if (!process_custom_command (cmd, ¶ms[1], session)) { + if ((custom_handler = parse_custom_command (cmd, ¶ms[1], session, 0)) == NULL) { msg_debug ("'%s'", cmd); i = rspamd_snprintf (out_buf, sizeof (out_buf), "Unknown command" CRLF); if (!rspamd_dispatcher_write (session->dispatcher, out_buf, i, FALSE, FALSE)) { return FALSE; } } + else { + custom_handler (¶ms[1], session); + } } } if (session->state != STATE_LEARN && session->state != STATE_LEARN_SPAM_PRE - && session->state != STATE_WEIGHTS && session->state != STATE_OTHER) { + && session->state != STATE_WEIGHTS && session->state != STATE_OTHER && session->state != STATE_HEADER) { if (!rspamd_dispatcher_write (session->dispatcher, END, sizeof (END) - 1, FALSE, TRUE)) { return FALSE; } @@ -928,6 +1066,43 @@ controller_read_socket (f_str_t * in, void *arg) } } + break; + case STATE_HEADER: + if (in->len == 0) { + /* End of headers */ + if (session->cmd == NULL && session->custom_handler == NULL) { + i = rspamd_snprintf (out_buf, sizeof (out_buf), "500 Bad command" CRLF); + if (!rspamd_dispatcher_write (session->dispatcher, out_buf, i, FALSE, FALSE)) { + return FALSE; + } + destroy_session (session->s); + return FALSE; + } + /* Perform command */ + else if (session->cmd != NULL) { + if (! process_command (session->cmd, NULL, session)) { + destroy_session (session->s); + return FALSE; + } + } + else { + session->custom_handler (NULL, session); + } + if (session->state != STATE_LEARN && session->state != STATE_LEARN_SPAM_PRE + && session->state != STATE_WEIGHTS && session->state != STATE_OTHER) { + destroy_session (session->s); + return FALSE; + } + } + if (!process_header (in, session)) { + msg_debug ("'%V'", in); + i = rspamd_snprintf (out_buf, sizeof (out_buf), "500 Bad header" CRLF); + if (!rspamd_dispatcher_write (session->dispatcher, out_buf, i, FALSE, FALSE)) { + return FALSE; + } + destroy_session (session->s); + return FALSE; + } break; case STATE_LEARN: session->learn_buf = in; @@ -1159,8 +1334,14 @@ controller_write_socket (void *arg) return TRUE; } else if (session->state == STATE_REPLY) { - session->state = STATE_COMMAND; - rspamd_set_dispatcher_policy (session->dispatcher, BUFFER_LINE, BUFSIZ); + if (session->restful) { + destroy_session (session->s); + return FALSE; + } + else { + session->state = STATE_COMMAND; + rspamd_set_dispatcher_policy (session->dispatcher, BUFFER_LINE, BUFSIZ); + } } rspamd_dispatcher_restore (session->dispatcher); return TRUE; diff --git a/src/fstring.c b/src/fstring.c index 07b5546a0..9f17ac861 100644 --- a/src/fstring.c +++ b/src/fstring.c @@ -218,7 +218,8 @@ fstrcstr (f_str_t * str, memory_pool_t * pool) gchar *res; res = memory_pool_alloc (pool, str->len + 1); - memcpy (res, str->begin, str->len); + /* Do not allow multiply \0 characters */ + memccpy (res, str->begin, '\0', str->len); res[str->len] = 0; return res; diff --git a/src/main.h b/src/main.h index f90edf1d2..9a28793cd 100644 --- a/src/main.h +++ b/src/main.h @@ -131,10 +131,15 @@ union sa_union { /** * Control session object */ +struct controller_command; +struct controller_session; +typedef void (*controller_func_t)(gchar **args, struct controller_session *session); + struct controller_session { struct rspamd_worker *worker; /**< pointer to worker structure (controller in fact) */ enum { STATE_COMMAND, + STATE_HEADER, STATE_LEARN, STATE_LEARN_SPAM_PRE, STATE_LEARN_SPAM, @@ -146,7 +151,10 @@ struct controller_session { } state; /**< current session state */ gint sock; /**< socket descriptor */ /* Access to authorized commands */ - gint authorized; /**< whether this session is authorized */ + gboolean authorized; /**< whether this session is authorized */ + gboolean restful; /**< whether this session is a restful session */ + GHashTable *kwargs; /**< keyword arguments for restful command */ + struct controller_command *cmd; /**< real command */ memory_pool_t *session_pool; /**< memory pool for session */ struct config_file *cfg; /**< pointer to config file */ gchar *learn_rcpt; /**< recipient for learning */ @@ -161,14 +169,13 @@ struct controller_session { void (*other_handler)(struct controller_session *session, f_str_t *in); /**< other command handler to execute at the end of processing */ void *other_data; /**< and its data */ + controller_func_t custom_handler; /**< custom command handler */ struct rspamd_async_session* s; /**< async session object */ struct worker_task *learn_task; struct rspamd_dns_resolver *resolver; /**< DNS resolver */ struct event_base *ev_base; /**< Event base */ }; -typedef void (*controller_func_t)(gchar **args, struct controller_session *session); - /** * Worker task structure */ diff --git a/src/protocol.c b/src/protocol.c index 4f6e0be38..7df9ae673 100644 --- a/src/protocol.c +++ b/src/protocol.c @@ -123,7 +123,7 @@ rspamc_proto_str (guint ver) } } -static gchar * +gchar * separate_command (f_str_t * in, gchar c) { guint r = 0; @@ -137,6 +137,11 @@ separate_command (f_str_t * in, gchar c) in->len -= r + 1; return b; } + else if (*p == '\0') { + /* Actually we cannot allow several \0 characters in string, so write to the log about it */ + msg_warn ("cannot separate command with \0 character, this can be an attack attempt"); + return NULL; + } p++; r++; } diff --git a/src/protocol.h b/src/protocol.h index de6d0ea03..72460940f 100644 --- a/src/protocol.h +++ b/src/protocol.h @@ -57,6 +57,14 @@ struct custom_command { protocol_reply_func func; }; +/** + * Find a character in command in and return pointer to the first part of the string, in is modified to point to the second part of string + * @param in f_str_t input + * @param c separator character + * @return pointer to the first part of string or NULL if there is no separator found + */ +gchar* separate_command (f_str_t * in, gchar c); + /** * Read one line of user's input for specified task * @param task task object diff --git a/src/smtp.c b/src/smtp.c index 6e226c42a..c1e8765e4 100644 --- a/src/smtp.c +++ b/src/smtp.c @@ -34,6 +34,7 @@ #include "message.h" #include "settings.h" #include "dns.h" +#include "lua/lua_common.h" /* Max line size as it is defined in rfc2822 */ #define OUTBUFSIZ 1000 @@ -309,6 +310,7 @@ process_smtp_data (struct smtp_session *session) session->task->fin_callback = smtp_write_socket; session->task->fin_arg = session; session->task->msg = memory_pool_alloc (session->pool, sizeof (f_str_t)); + session->task->s = session->s; #ifdef HAVE_MMAP_NOCORE if ((session->task->msg->begin = mmap (NULL, st.st_size, PROT_READ, MAP_SHARED | MAP_NOCORE, session->temp_fd, 0)) == MAP_FAILED) { #else @@ -346,23 +348,22 @@ process_smtp_data (struct smtp_session *session) if (process_message (session->task) == -1) { msg_err ("cannot process message"); munmap (session->task->msg->begin, st.st_size); - msg_err ("process message failed: %s", strerror (errno)); goto err; } - r = process_filters (session->task); - if (r == -1) { - munmap (session->task->msg->begin, st.st_size); - msg_err ("cannot process filters"); - goto err; - } - else if (r == 0) { - session->state = SMTP_STATE_END; - rspamd_dispatcher_pause (session->dispatcher); + if (session->task->cfg->pre_filters == NULL) { + r = process_filters (session->task); + if (r == -1) { + msg_err ("cannot process message"); + munmap (session->task->msg->begin, st.st_size); + goto err; + } } else { - process_statfiles (session->task); - session->state = SMTP_STATE_END; - return smtp_write_socket (session); + lua_call_pre_filters (session->task); + /* We want fin_task after pre filters are processed */ + session->task->s->wanna_die = TRUE; + session->task->state = WAIT_PRE_FILTER; + check_session_pending (session->task->s); } } else { diff --git a/src/smtp_proto.c b/src/smtp_proto.c index 2211411e1..e1e7f3229 100644 --- a/src/smtp_proto.c +++ b/src/smtp_proto.c @@ -394,6 +394,7 @@ smtp_upstream_read_socket (f_str_t * in, void *arg) gchar outbuf[BUFSIZ]; gint r; + msg_debug ("in: %V, state: %d", in, session->upstream_state); switch (session->upstream_state) { case SMTP_STATE_GREETING: r = check_smtp_ustream_reply (in, '2'); -- 2.39.5