diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 000000000..0d0addb84 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,4 @@ +# GitHub Actions workflows + +- **codeql.yml** – CodeQL analysis (push/PR to master, weekly). +- **make-multi-platform.yml** – Build with Make on Rocky Linux 10 (server/agent) and Windows agent (cross-compile). Based on the multi-platform CI idea from PR #2158 by @cmac9203; adapted for this project’s Make build. diff --git a/.github/workflows/make-multi-platform.yml b/.github/workflows/make-multi-platform.yml new file mode 100644 index 000000000..114929f88 --- /dev/null +++ b/.github/workflows/make-multi-platform.yml @@ -0,0 +1,55 @@ +# Multi-platform build workflow. +# Initial CI structure and idea from PR #2158 by @cmac9203. Adapted to use the +# project's Make-based build (OSSEC-HIDS does not use CMake). +name: Build (multi-platform) + +on: + push: + branches: [ "main", "master" ] + pull_request: + branches: [ "main", "master" ] + +jobs: + build-rocky: + name: Rocky Linux 10 (${{ matrix.target }}) + runs-on: ubuntu-latest + container: rockylinux:10 + strategy: + fail-fast: false + matrix: + target: [ server, agent ] + + steps: + - uses: actions/checkout@v4 + + - name: Install build dependencies + run: | + dnf install -y gcc make openssl-devel pcre2-devel zlib-devel \ + systemd-devel libevent-devel + dnf install -y file-devel || true + + - name: Build + run: | + cd src + make TARGET=${{ matrix.target }} PCRE2_SYSTEM=yes -j$(nproc) + + build-windows-agent: + name: Windows agent (cross-compile) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install MinGW and deps + run: | + sudo apt-get update -qq + sudo apt-get install -y build-essential mingw-w64 libssl-dev + + - name: Fetch PCRE2 for Windows build + run: | + mkdir -p src/external + wget -q https://github.com/PCRE2Project/pcre2/releases/download/pcre2-10.32/pcre2-10.32.tar.gz -O - | tar xz -C src/external + + - name: Build Windows agent + run: | + cd src + make TARGET=winagent -j$(nproc) diff --git a/src/Makefile b/src/Makefile index 0d6ea2b20..bf48be390 100644 --- a/src/Makefile +++ b/src/Makefile @@ -27,6 +27,7 @@ INSTALL_RESOLVCONF?=yes USE_PRELUDE?=no USE_ZEROMQ?=no USE_GEOIP?=no +USE_CURL?=no USE_INOTIFY=no USE_PCRE2_JIT=yes USE_SYSTEMD?=yes @@ -259,6 +260,11 @@ ifneq (,$(filter ${USE_GEOIP},auto yes y Y 1)) OSSEC_LDFLAGS+=-lGeoIP endif # USE_GEOIP +ifneq (,$(filter ${USE_CURL},yes y Y 1)) + DEFINES+=-DUSE_SMTP_CURL + OSSEC_LDFLAGS+=-lcurl +endif # USE_CURL + ifneq (,$(filter ${USE_SQLITE},auto yes y Y 1)) DEFINES+=-DSQLITE_ENABLED ANALYSISD_FLAGS="-lsqlite3" @@ -618,6 +624,7 @@ settings: @echo "USE settings:" @echo " USE_ZEROMQ: ${USE_ZEROMQ}" @echo " USE_GEOIP: ${USE_GEOIP}" + @echo " USE_CURL: ${USE_CURL}" @echo " USE_PRELUDE: ${USE_PRELUDE}" @echo " USE_OPENSSL: ${USE_OPENSSL}" @echo " USE_INOTIFY: ${USE_INOTIFY}" diff --git a/src/config/global-config.c b/src/config/global-config.c index 0449a8c94..47a78477a 100644 --- a/src/config/global-config.c +++ b/src/config/global-config.c @@ -122,6 +122,11 @@ int Read_Global(XML_NODE node, void *configp, void *mailp) const char *xml_smtpserver = "smtp_server"; const char *xml_heloserver = "helo_server"; const char *xml_mailmaxperhour = "email_maxperhour"; + const char *xml_auth_smtp = "auth_smtp"; + const char *xml_smtp_user = "smtp_user"; + const char *xml_smtp_password = "smtp_password"; + const char *xml_secure_smtp = "secure_smtp"; + const char *xml_smtp_port = "smtp_port"; #ifdef LIBGEOIP_ENABLED const char *xml_geoip_db_path = "geoip_db_path"; @@ -453,17 +458,82 @@ int Read_Global(XML_NODE node, void *configp, void *mailp) } os_strdup(node[i]->content, Mail->idsname); } + } else if (strcmp(node[i]->element, xml_auth_smtp) == 0) { + if (strcmp(node[i]->content, "yes") == 0) { + if (Config) { + Config->authsmtp = 1; + } + if (Mail) { + Mail->authsmtp = 1; + } + } else if (strcmp(node[i]->content, "no") == 0) { + if (Config) { + Config->authsmtp = 0; + } + if (Mail) { + Mail->authsmtp = 0; + } + } else { + return (OS_INVALID); + } + } else if (strcmp(node[i]->element, xml_secure_smtp) == 0) { + if (strcmp(node[i]->content, "yes") == 0) { + if (Config) { + Config->securesmtp = 1; + } + if (Mail) { + Mail->securesmtp = 1; + } + } else if (strcmp(node[i]->content, "no") == 0) { + if (Config) { + Config->securesmtp = 0; + } + if (Mail) { + Mail->securesmtp = 0; + } + } else { + return (OS_INVALID); + } + } else if (strcmp(node[i]->element, xml_smtp_user) == 0) { + if (Mail) { + if (Mail->smtp_user) { + free(Mail->smtp_user); + } + os_strdup(node[i]->content, Mail->smtp_user); + } + } else if (strcmp(node[i]->element, xml_smtp_password) == 0) { + if (Mail) { + if (Mail->smtp_pass) { + memset_secure(Mail->smtp_pass, 0, strlen(Mail->smtp_pass)); + free(Mail->smtp_pass); + } + os_strdup(node[i]->content, Mail->smtp_pass); + } } else if (strcmp(node[i]->element, xml_smtpserver) == 0) { #ifndef WIN32 if (Mail && (Mail->mn)) { if (node[i]->content[0] == '/') { os_strdup(node[i]->content, Mail->smtpserver); } else { +#ifdef USE_SMTP_CURL + /* Pre-resolve for CURLOPT_RESOLVE; DNS is unavailable after chroot */ + if (Mail->smtpserver_resolved) { + free(Mail->smtpserver_resolved); + Mail->smtpserver_resolved = NULL; + } + Mail->smtpserver_resolved = OS_GetHost(node[i]->content, 5); + if (!Mail->smtpserver_resolved) { + merror(INVALID_SMTP, __local_name, node[i]->content); + return (OS_INVALID); + } + /* Hostname as-is for libcurl; common free + os_strdup below */ +#else Mail->smtpserver = OS_GetHost(node[i]->content, 5); if (!Mail->smtpserver) { merror(INVALID_SMTP, __local_name, node[i]->content); return (OS_INVALID); } +#endif } free(Mail->smtpserver); os_strdup(node[i]->content, Mail->smtpserver); @@ -473,6 +543,18 @@ int Read_Global(XML_NODE node, void *configp, void *mailp) if (Mail && (Mail->mn)) { os_strdup(node[i]->content, Mail->heloserver); } + } else if (strcmp(node[i]->element, xml_smtp_port) == 0) { + if (Mail && (Mail->mn)) { + if (!OS_StrIsNum(node[i]->content)) { + merror(XML_VALUEERR, __local_name, node[i]->element, node[i]->content); + return (OS_INVALID); + } + Mail->smtp_port = atoi(node[i]->content); + if (Mail->smtp_port < 1 || Mail->smtp_port > 65535) { + merror(XML_VALUEERR, __local_name, node[i]->element, node[i]->content); + return (OS_INVALID); + } + } } else if (strcmp(node[i]->element, xml_mailmaxperhour) == 0) { if (Mail) { if (!OS_StrIsNum(node[i]->content)) { diff --git a/src/config/global-config.h b/src/config/global-config.h index 774466335..501f67284 100644 --- a/src/config/global-config.h +++ b/src/config/global-config.h @@ -53,6 +53,10 @@ typedef struct __Config { /* Mail alerting */ short int mailnotify; + /* SMTP auth (USE_CURL build only) */ + short int authsmtp; + short int securesmtp; + /* Custom Alert output*/ short int custom_alert_output; char *custom_alert_output_format; diff --git a/src/config/mail-config.h b/src/config/mail-config.h index f5fe6f332..0c7ce6905 100644 --- a/src/config/mail-config.h +++ b/src/config/mail-config.h @@ -34,6 +34,14 @@ typedef struct _MailConfig { char *idsname; char *smtpserver; char *heloserver; + char *smtpserver_resolved; /* pre-resolved IP for CURLOPT_RESOLVE when chrooted (USE_SMTP_CURL) */ + + /* SMTP auth (USE_CURL build only) */ + int authsmtp; /* 0 = off (default), 1 = on */ + int securesmtp; /* 0 = off (default), 1 = on */ + int smtp_port; /* 0 = use default per mode (465/587/25); else override */ + char *smtp_user; + char *smtp_pass; /* Granular e-mail options */ unsigned int *gran_level; diff --git a/src/monitord/main.c b/src/monitord/main.c index 28e983017..6dd3543d5 100644 --- a/src/monitord/main.c +++ b/src/monitord/main.c @@ -145,7 +145,22 @@ int main(int argc, char **argv) mond.emailidsname = OS_GetOneContentforElement(&xml, xml_idsname); if (tmpsmtp && mond.emailfrom) { +#ifdef USE_SMTP_CURL + /* Validate hostname with OS_GetHost() but store original string for libcurl. */ + if (tmpsmtp[0] != '/') { + char *validated_host = OS_GetHost(tmpsmtp, 5); + if (!validated_host) { + mond.smtpserver = NULL; + } else { + free(validated_host); + os_strdup(tmpsmtp, mond.smtpserver); + } + } else { + os_strdup(tmpsmtp, mond.smtpserver); + } +#else mond.smtpserver = OS_GetHost(tmpsmtp, 5); +#endif if (!mond.smtpserver) { merror(INVALID_SMTP, ARGV0, tmpsmtp); if (mond.emailfrom) { @@ -154,6 +169,8 @@ int main(int argc, char **argv) mond.emailfrom = NULL; merror("%s: Invalid SMTP server. Disabling email reports.", ARGV0); } + free(tmpsmtp); + tmpsmtp = NULL; } else { if (tmpsmtp) { free(tmpsmtp); diff --git a/src/os_maild/config.c b/src/os_maild/config.c index 13f2f2f6f..6cb3d9378 100644 --- a/src/os_maild/config.c +++ b/src/os_maild/config.c @@ -25,6 +25,12 @@ int MailConf(int test_config, const char *cfgfile, MailConfig *Mail) Mail->idsname = NULL; Mail->smtpserver = NULL; Mail->heloserver = NULL; + Mail->smtpserver_resolved = NULL; + Mail->authsmtp = 0; + Mail->securesmtp = 0; + Mail->smtp_port = 0; + Mail->smtp_user = NULL; + Mail->smtp_pass = NULL; Mail->mn = 0; Mail->priority = 0; Mail->maxperhour = 12; @@ -45,6 +51,19 @@ int MailConf(int test_config, const char *cfgfile, MailConfig *Mail) return (OS_INVALID); } +#ifndef USE_SMTP_CURL + if (Mail->authsmtp || Mail->securesmtp || Mail->smtp_port || + Mail->smtp_user || Mail->smtp_pass) { + merror("%s: SMTP authentication/TLS options (auth_smtp, secure_smtp, smtp_port, smtp_user, smtp_password) require building with USE_CURL=yes.", ARGV0); + return (OS_INVALID); + } +#else + if (Mail->authsmtp && (!Mail->smtp_user || !Mail->smtp_pass)) { + merror("%s: auth_smtp=yes requires both smtp_user and smtp_password to be set.", ARGV0); + return (OS_INVALID); + } +#endif + if (!Mail->mn) { if (!test_config) { verbose(MAIL_DIS, ARGV0); diff --git a/src/os_maild/curlmail.c b/src/os_maild/curlmail.c new file mode 100644 index 000000000..bf5d7f6b5 --- /dev/null +++ b/src/os_maild/curlmail.c @@ -0,0 +1,332 @@ +/* Copyright (C) Atomicorp, Inc. + * All rights reserved. + * + * This program is a free software; you can redistribute it + * and/or modify it under the terms of the GNU General Public + * License (version 2) as published by the FSF - Free Software + * Foundation. + * + * SMTP send via libcurl (TLS + AUTH). Built only when USE_SMTP_CURL is defined. + */ + +#ifdef USE_SMTP_CURL + +#include +#include +#include +#include +#include +#include "shared.h" +#include "maild.h" +#include "mail_list.h" + +#define HEADER_MAX 2048 +#define SMTP_URL_MAX 512 +#define HOSTNAME_MAX 253 +#define CONNECT_TIMEOUT 30 +#define TRANSFER_TIMEOUT 120 + +typedef struct _smtp_payload_ctx { + const char *header; + size_t header_len; + size_t header_sent; + const char *body; + size_t body_len; + size_t body_sent; +} smtp_payload_ctx; + +/* Copy at most dst_size-1 chars from src into dst, stripping CR/LF to prevent header injection. */ +static void sanitize_header_value(const char *src, char *dst, size_t dst_size) +{ + size_t j = 0; + if (!dst || dst_size == 0) return; + if (!src) { dst[0] = '\0'; return; } + while (src[0] && j < dst_size - 1) { + if (src[0] != '\r' && src[0] != '\n') { + dst[j++] = src[0]; + } + src++; + } + dst[j] = '\0'; +} + +/* Allow only hostname-safe chars (alphanumeric, hyphen, dot). Reject empty and overlength. */ +static int is_valid_smtp_host(const char *host) +{ + size_t n = 0; + if (!host || !host[0]) return 0; + for (; host[n] && n <= HOSTNAME_MAX; n++) { + if (!isalnum((unsigned char)host[n]) && host[n] != '-' && host[n] != '.') { + return 0; + } + } + return (host[n] == '\0' && n > 0 && n <= HOSTNAME_MAX); +} + +static size_t payload_source(void *ptr, size_t size, size_t nmemb, void *userp) +{ + smtp_payload_ctx *ctx = (smtp_payload_ctx *)userp; + size_t want; + size_t sent = 0; + + if (size == 0 || nmemb > SIZE_MAX / size) { + return 0; + } + want = size * nmemb; + + if (ctx->header_sent < ctx->header_len) { + size_t left = ctx->header_len - ctx->header_sent; + sent = (want < left) ? want : left; + memcpy(ptr, ctx->header + ctx->header_sent, sent); + ctx->header_sent += sent; + return sent; + } + + if (ctx->body_sent < ctx->body_len) { + size_t left = ctx->body_len - ctx->body_sent; + sent = (want < left) ? want : left; + memcpy(ptr, ctx->body + ctx->body_sent, sent); + ctx->body_sent += sent; + return sent; + } + + return 0; +} + +static int send_one_mail(CURL *curl, MailConfig *mail, struct tm *p, MailMsg *msg) +{ + struct curl_slist *recipients = NULL; + struct curl_slist *resolve_list = NULL; + char url[SMTP_URL_MAX]; + char header_buf[HEADER_MAX]; + char date_buf[64]; + char message_id[128]; + char hostname[256]; + char sanitized_subject[512]; + char sanitized_from[384]; + char sanitized_to[384]; + smtp_payload_ctx ctx; + CURLcode res; + unsigned int i; + size_t body_len; + int ret = -1; + + curl_easy_reset(curl); + + if (!mail->smtpserver || !mail->from || !mail->to || !mail->to[0]) { + merror("%s: Incomplete mail config (smtp_server, email_from, email_to).", ARGV0); + return -1; + } + + if (!is_valid_smtp_host(mail->smtpserver)) { + merror("%s: Invalid smtp_server (hostname only, no path or invalid chars).", ARGV0); + return -1; + } + + if (mail->authsmtp && (!mail->smtp_user || !mail->smtp_pass)) { + merror("%s: auth_smtp=yes requires smtp_user and smtp_password to be set.", ARGV0); + return -1; + } + + /* Build URL: optional smtp_port overrides defaults (465/587/25 per mode) */ + { + int port = mail->smtp_port; + int n; + if (port <= 0 || port > 65535) { + if (mail->securesmtp) { + port = 465; + } else if (mail->authsmtp) { + port = 587; + } else { + port = 25; + } + } + if (mail->securesmtp) { + n = snprintf(url, sizeof(url), "smtps://%s:%d", mail->smtpserver, port); + } else { + n = snprintf(url, sizeof(url), "smtp://%s:%d", mail->smtpserver, port); + } + if (n < 0 || (size_t)n >= sizeof(url)) { + merror("%s: smtp_server or URL too long (truncation).", ARGV0); + return -1; + } + /* Pre-resolved IP for chroot (no DNS inside jail); hostname:port:ip for CURLOPT_RESOLVE */ + if (mail->smtpserver_resolved) { + char resolve_buf[384]; + n = snprintf(resolve_buf, sizeof(resolve_buf), "%s:%d:%s", mail->smtpserver, port, mail->smtpserver_resolved); + if (n < 0 || (size_t)n >= sizeof(resolve_buf)) { + merror("%s: smtp_server or resolved IP too long for CURLOPT_RESOLVE (truncation).", ARGV0); + return -1; + } + resolve_list = curl_slist_append(NULL, resolve_buf); + if (!resolve_list) { + merror("%s: Failed to build resolve list for chroot (CURLOPT_RESOLVE).", ARGV0); + return -1; + } + curl_easy_setopt(curl, CURLOPT_RESOLVE, resolve_list); + } + } + + if (!mail->securesmtp && !mail->authsmtp) { + verbose("%s: Sending mail via unencrypted SMTP (smtp://%s:%d).", ARGV0, mail->smtpserver, + mail->smtp_port > 0 ? mail->smtp_port : 25); + } + + /* Recipient list: mail->to[] and mail->gran_to[] (FULL_FORMAT only). + * Reject any recipient containing CR/LF to prevent SMTP command injection + * (older libcurl may not sanitize CURLOPT_MAIL_RCPT). Use a temp so on + * append failure we keep the list and free it in done. */ + { + struct curl_slist *new_node; + for (i = 0; mail->to[i] != NULL; i++) { + if (strchr(mail->to[i], '\r') || strchr(mail->to[i], '\n')) { + merror("%s: Skipping recipient with invalid CR/LF (SMTP injection attempt or misconfiguration).", ARGV0); + continue; + } + new_node = curl_slist_append(recipients, mail->to[i]); + if (!new_node) { + merror("%s: Failed to append recipient.", ARGV0); + goto done; + } + recipients = new_node; + } + if (mail->gran_to && mail->gran_set) { + for (i = 0; mail->gran_to[i] != NULL; i++) { + if (mail->gran_set[i] == FULL_FORMAT) { + if (strchr(mail->gran_to[i], '\r') || strchr(mail->gran_to[i], '\n')) { + merror("%s: Skipping granular recipient with invalid CR/LF.", ARGV0); + continue; + } + new_node = curl_slist_append(recipients, mail->gran_to[i]); + if (!new_node) { + merror("%s: Failed to append granular recipient.", ARGV0); + goto done; + } + recipients = new_node; + } + } + } + } + + if (!recipients) { + merror("%s: No recipients.", ARGV0); + goto done; + } + + hostname[0] = '\0'; + if (gethostname(hostname, sizeof(hostname)) != 0) { + strncpy(hostname, "localhost", sizeof(hostname) - 1); + hostname[sizeof(hostname) - 1] = '\0'; + } else { + hostname[sizeof(hostname) - 1] = '\0'; + } + strftime(message_id, sizeof(message_id), "%Y%m%d%H%M%S.%z", p); + strftime(date_buf, sizeof(date_buf), "%a, %d %b %Y %T %z", p); + + sanitize_header_value(msg->subject ? msg->subject : "", sanitized_subject, sizeof(sanitized_subject)); + sanitize_header_value(mail->from, sanitized_from, sizeof(sanitized_from)); + sanitize_header_value(mail->to[0], sanitized_to, sizeof(sanitized_to)); + + body_len = msg->body ? strlen(msg->body) : 0; + { + int n = snprintf(header_buf, sizeof(header_buf), + "Date: %s\r\n" + "To: %s\r\n" + "From: %s\r\n" + "Message-ID: <%s@%s>\r\n" + "Subject: %s\r\n" + "\r\n", + date_buf, + sanitized_to, + sanitized_from, + message_id, + hostname, + sanitized_subject); + if (n < 0 || (size_t)n >= sizeof(header_buf)) { + merror("%s: Email header truncated (subject/from/to too long).", ARGV0); + goto done; + } + } + + ctx.header = header_buf; + ctx.header_len = strlen(header_buf); + ctx.header_sent = 0; + ctx.body = msg->body ? msg->body : ""; + ctx.body_len = body_len; + ctx.body_sent = 0; + + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_MAIL_FROM, sanitized_from); + curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, recipients); + curl_easy_setopt(curl, CURLOPT_READFUNCTION, payload_source); + curl_easy_setopt(curl, CURLOPT_READDATA, &ctx); + curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); + /* Do not use alarm() for timeouts; ossec-maild uses SIGTERM/SIGHUP and may use alarm() */ + curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, (long)CONNECT_TIMEOUT); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)TRANSFER_TIMEOUT); + /* Explicit TLS verification so behavior is not dependent on libcurl defaults */ + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); + /* No CURLOPT_VERBOSE - do not log to stderr */ + /* No CURLOPT_CAINFO - use system default CA bundle */ + + if (mail->authsmtp && mail->smtp_user && mail->smtp_pass) { + curl_easy_setopt(curl, CURLOPT_USERNAME, mail->smtp_user); + curl_easy_setopt(curl, CURLOPT_PASSWORD, mail->smtp_pass); + /* Require TLS when using AUTH so credentials are not sent in plaintext */ + curl_easy_setopt(curl, CURLOPT_USE_SSL, (long)CURLUSESSL_ALL); + } + + if (mail->securesmtp) { + curl_easy_setopt(curl, CURLOPT_USE_SSL, (long)CURLUSESSL_ALL); + } + + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + merror("%s: curl_easy_perform failed: %s", ARGV0, curl_easy_strerror(res)); + goto done; + } + + ret = 0; +done: + curl_slist_free_all(recipients); + curl_slist_free_all(resolve_list); + /* message_id is date+hostname only, no secrets */ + return ret; +} + +int OS_Sendmail(MailConfig *mail, struct tm *p) +{ + CURL *curl = NULL; + MailNode *mailmsg; + int ret = 0; + + if (!mail || !p) { + return (OS_INVALID); + } + + if (mail->smtpserver && mail->smtpserver[0] == '/') { + /* Local sendmail path not supported in curl build; would need fallback to plain path */ + merror("%s: Local sendmail path not supported when built with USE_CURL.", ARGV0); + return (OS_INVALID); + } + + curl = curl_easy_init(); + if (!curl) { + merror("%s: curl_easy_init failed.", ARGV0); + return (OS_INVALID); + } + + while ((mailmsg = OS_PopLastMail()) != NULL) { + if (send_one_mail(curl, mail, p, mailmsg->mail) < 0) { + ret = OS_INVALID; + } + FreeMail(mailmsg); + } + + curl_easy_cleanup(curl); + return ret; +} + +#endif /* USE_SMTP_CURL */ diff --git a/src/os_maild/maild.c b/src/os_maild/maild.c index 305c078c3..857819d8d 100644 --- a/src/os_maild/maild.c +++ b/src/os_maild/maild.c @@ -10,6 +10,10 @@ #include "shared.h" #include "maild.h" #include "mail_list.h" +#ifdef USE_SMTP_CURL +#include +#include "os_net/os_net.h" +#endif #ifndef ARGV0 #define ARGV0 "ossec-maild" @@ -24,6 +28,26 @@ char _g_subject[SUBJECT_SIZE + 2]; static void OS_Run(MailConfig *mail) __attribute__((nonnull)) __attribute__((noreturn)); static void help_maild(int status) __attribute__((noreturn)); +#ifdef USE_SMTP_CURL +static MailConfig *s_mail_cleanup = NULL; +static void maild_clear_smtp_secrets(void) +{ + if (s_mail_cleanup) { + if (s_mail_cleanup->smtp_user) { + memset_secure(s_mail_cleanup->smtp_user, 0, strlen(s_mail_cleanup->smtp_user)); + } + if (s_mail_cleanup->smtp_pass) { + memset_secure(s_mail_cleanup->smtp_pass, 0, strlen(s_mail_cleanup->smtp_pass)); + } + if (s_mail_cleanup->smtpserver_resolved) { + free(s_mail_cleanup->smtpserver_resolved); + s_mail_cleanup->smtpserver_resolved = NULL; + } + } + curl_global_cleanup(); +} +#endif + /* Print help statement */ static void help_maild(int status) @@ -123,6 +147,14 @@ int main(int argc, char **argv) ErrorExit(CONFIG_ERROR, ARGV0, cfg); } +#ifdef USE_SMTP_CURL + if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) { + ErrorExit("%s: curl_global_init failed.", ARGV0); + } + s_mail_cleanup = &mail; + atexit(maild_clear_smtp_secrets); +#endif + /* Read internal options */ mail.strict_checking = getDefine_Int("maild", "strict_checking", @@ -160,7 +192,19 @@ int main(int argc, char **argv) ErrorExit(SETGID_ERROR, ARGV0, group, errno, strerror(errno)); } - if (mail.smtpserver[0] != '/') { +#ifdef USE_SMTP_CURL + /* Pre-resolve SMTP hostname before chroot; DNS is unavailable inside the jail. + * global-config.c also resolves at parse time, but verify here as a safeguard. */ + if (mail.smtpserver && mail.smtpserver[0] != '/' && !mail.smtpserver_resolved) { + mail.smtpserver_resolved = OS_GetHost(mail.smtpserver, 5); + if (!mail.smtpserver_resolved) { + merror("%s: Could not resolve smtp_server '%s'. DNS will not work after chroot.", ARGV0, mail.smtpserver); + ErrorExit(CONFIG_ERROR, ARGV0, cfg); + } + } +#endif + + if (mail.smtpserver && mail.smtpserver[0] != '/') { /* chroot */ if (Privsep_Chroot(dir) < 0) { ErrorExit(CHROOT_ERROR, ARGV0, dir, errno, strerror(errno)); @@ -268,7 +312,15 @@ static void OS_Run(MailConfig *mail) if (OS_Sendmail(mail, p) < 0) { merror(SNDMAIL_ERROR, ARGV0, mail->smtpserver); } - +#ifdef USE_SMTP_CURL + /* Clear credentials from child memory before exit (not zeroed by atexit in time) */ + if (mail->smtp_user) { + memset_secure(mail->smtp_user, 0, strlen(mail->smtp_user)); + } + if (mail->smtp_pass) { + memset_secure(mail->smtp_pass, 0, strlen(mail->smtp_pass)); + } +#endif exit(0); } diff --git a/src/os_maild/sendmail.c b/src/os_maild/sendmail.c index f9221bb51..1b3a6b26b 100644 --- a/src/os_maild/sendmail.c +++ b/src/os_maild/sendmail.c @@ -7,7 +7,9 @@ * Foundation */ -/* Basic e-mailing operations */ +/* Basic e-mailing operations (plain TCP / local sendmail). Not used when USE_SMTP_CURL is defined. */ + +#ifndef USE_SMTP_CURL #include "shared.h" #include "os_net/os_net.h" @@ -392,3 +394,5 @@ int OS_Sendmail(MailConfig *mail, struct tm *p) memset_secure(snd_msg, '\0', 128); return (0); } + +#endif /* !USE_SMTP_CURL */