From ddcd096f99641df9368781481f21f0aaa2e030a9 Mon Sep 17 00:00:00 2001 From: "Scott R. Shinn" Date: Sat, 14 Mar 2026 15:48:10 -0400 Subject: [PATCH 1/4] Updating for multi-platform Starting on rocky. Based on PR #2158 by @cmac9203. Refactoring for make Signed-off-by: Scott R. Shinn --- .github/workflows/README.md | 4 ++ .github/workflows/make-multi-platform.yml | 55 +++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 .github/workflows/README.md create mode 100644 .github/workflows/make-multi-platform.yml 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) From 4a72744e9356533d26db0dcb0075c546f1f855b2 Mon Sep 17 00:00:00 2001 From: "Scott R. Shinn" Date: Sat, 14 Mar 2026 18:14:35 -0400 Subject: [PATCH 2/4] Add SMTP TLS and authentication Enable authenticated and TLS SMTP for ossec-maild when built with USE_CURL=yes (off by default). Uses libcurl for SMTP AUTH (PLAIN/LOGIN) and TLS/STARTTLS; credentials and TLS are validated and sanitized. Security hardening: header/envelope CR/LF sanitization, hostname validation for smtp_server, timeouts, mandatory TLS when AUTH is on, post-parse credential validation, and secure clearing of password in config and at exit. CA bundle and chroot ossec-maild runs inside a chroot (e.g. /var/ossec). libcurl uses CURLOPT_SSL_VERIFYPEER=1 and by default looks for the system CA bundle (e.g. /etc/ssl/certs/ca-certificates.crt). After chroot, that path is not visible, so TLS verification fails (CURLE_PEER_FAILED_VERIFICATION) and mail is dropped unless the CA bundle is available inside the chroot. Installation (or the admin) must copy or symlink the system CA bundle into the chroot (e.g. /etc/ssl/certs/ca-certificates.crt) and either set CURLOPT_CAINFO to that path in code or ensure the default path resolves inside the chroot. Do not disable VERIFYPEER. Original idea and initial implementation from alexbartlow via Allow TLS Email sends as a compile-time option https://github.com/ossec/ossec-hids/pull/1360 Credit: alexbartlow (PR #1360) Signed-off-by: Scott R. Shinn --- src/Makefile | 7 + src/config/global-config.c | 82 ++++++++++ src/config/global-config.h | 4 + src/config/mail-config.h | 8 + src/monitord/main.c | 6 + src/os_maild/config.c | 18 +++ src/os_maild/curlmail.c | 315 +++++++++++++++++++++++++++++++++++++ src/os_maild/maild.c | 49 +++++- src/os_maild/sendmail.c | 6 +- 9 files changed, 493 insertions(+), 2 deletions(-) create mode 100644 src/os_maild/curlmail.c 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..714e30ff4 100644 --- a/src/monitord/main.c +++ b/src/monitord/main.c @@ -145,7 +145,11 @@ int main(int argc, char **argv) mond.emailidsname = OS_GetOneContentforElement(&xml, xml_idsname); if (tmpsmtp && mond.emailfrom) { +#ifdef USE_SMTP_CURL + 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 +158,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..e1684837e 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,18 @@ int MailConf(int test_config, const char *cfgfile, MailConfig *Mail) return (OS_INVALID); } +#ifndef USE_SMTP_CURL + if (Mail->authsmtp) { + merror("%s: SMTP authentication (auth_smtp=yes) requires 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..1034c3f7f --- /dev/null +++ b/src/os_maild/curlmail.c @@ -0,0 +1,315 @@ +/* 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; + if (port <= 0 || port > 65535) { + if (mail->securesmtp) { + port = 465; + } else if (mail->authsmtp) { + port = 587; + } else { + port = 25; + } + } + if (mail->securesmtp) { + snprintf(url, sizeof(url), "smtps://%s:%d", mail->smtpserver, port); + } else { + snprintf(url, sizeof(url), "smtp://%s:%d", mail->smtpserver, port); + } + /* Pre-resolved IP for chroot (no DNS inside jail); hostname:port:ip for CURLOPT_RESOLVE */ + if (mail->smtpserver_resolved) { + char resolve_buf[384]; + snprintf(resolve_buf, sizeof(resolve_buf), "%s:%d:%s", mail->smtpserver, port, mail->smtpserver_resolved); + resolve_list = curl_slist_append(NULL, resolve_buf); + if (resolve_list) { + 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; + 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); + + 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..bb33015cf 100644 --- a/src/os_maild/maild.c +++ b/src/os_maild/maild.c @@ -10,6 +10,9 @@ #include "shared.h" #include "maild.h" #include "mail_list.h" +#ifdef USE_SMTP_CURL +#include "os_net/os_net.h" +#endif #ifndef ARGV0 #define ARGV0 "ossec-maild" @@ -24,6 +27,25 @@ 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; + } + } +} +#endif + /* Print help statement */ static void help_maild(int status) @@ -123,6 +145,11 @@ int main(int argc, char **argv) ErrorExit(CONFIG_ERROR, ARGV0, cfg); } +#ifdef USE_SMTP_CURL + s_mail_cleanup = &mail; + atexit(maild_clear_smtp_secrets); +#endif + /* Read internal options */ mail.strict_checking = getDefine_Int("maild", "strict_checking", @@ -160,6 +187,18 @@ int main(int argc, char **argv) ErrorExit(SETGID_ERROR, ARGV0, group, errno, strerror(errno)); } +#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[0] != '/') { /* chroot */ if (Privsep_Chroot(dir) < 0) { @@ -268,7 +307,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 */ From 9e1fa8b67876040a095537eae7974f0eb69d8080 Mon Sep 17 00:00:00 2001 From: "Scott R. Shinn" Date: Sat, 14 Mar 2026 18:46:41 -0400 Subject: [PATCH 3/4] Code fixes Signed-off-by: Scott R. Shinn --- src/monitord/main.c | 13 +++++++++- src/os_maild/config.c | 5 ++-- src/os_maild/curlmail.c | 53 +++++++++++++++++++++++++++-------------- src/os_maild/maild.c | 7 +++++- 4 files changed, 56 insertions(+), 22 deletions(-) diff --git a/src/monitord/main.c b/src/monitord/main.c index 714e30ff4..6dd3543d5 100644 --- a/src/monitord/main.c +++ b/src/monitord/main.c @@ -146,7 +146,18 @@ int main(int argc, char **argv) if (tmpsmtp && mond.emailfrom) { #ifdef USE_SMTP_CURL - os_strdup(tmpsmtp, mond.smtpserver); + /* 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 diff --git a/src/os_maild/config.c b/src/os_maild/config.c index e1684837e..6cb3d9378 100644 --- a/src/os_maild/config.c +++ b/src/os_maild/config.c @@ -52,8 +52,9 @@ int MailConf(int test_config, const char *cfgfile, MailConfig *Mail) } #ifndef USE_SMTP_CURL - if (Mail->authsmtp) { - merror("%s: SMTP authentication (auth_smtp=yes) requires building with USE_CURL=yes.", ARGV0); + 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 diff --git a/src/os_maild/curlmail.c b/src/os_maild/curlmail.c index 1034c3f7f..bf5d7f6b5 100644 --- a/src/os_maild/curlmail.c +++ b/src/os_maild/curlmail.c @@ -131,6 +131,7 @@ static int send_one_mail(CURL *curl, MailConfig *mail, struct tm *p, MailMsg *ms /* 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; @@ -141,18 +142,28 @@ static int send_one_mail(CURL *curl, MailConfig *mail, struct tm *p, MailMsg *ms } } if (mail->securesmtp) { - snprintf(url, sizeof(url), "smtps://%s:%d", mail->smtpserver, port); + n = snprintf(url, sizeof(url), "smtps://%s:%d", mail->smtpserver, port); } else { - snprintf(url, sizeof(url), "smtp://%s:%d", mail->smtpserver, port); + 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]; - snprintf(resolve_buf, sizeof(resolve_buf), "%s:%d:%s", mail->smtpserver, port, mail->smtpserver_resolved); + 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) { - curl_easy_setopt(curl, CURLOPT_RESOLVE, resolve_list); + 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); } } @@ -217,19 +228,25 @@ static int send_one_mail(CURL *curl, MailConfig *mail, struct tm *p, MailMsg *ms sanitize_header_value(mail->to[0], sanitized_to, sizeof(sanitized_to)); body_len = msg->body ? strlen(msg->body) : 0; - 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); + { + 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); diff --git a/src/os_maild/maild.c b/src/os_maild/maild.c index bb33015cf..592fbd67c 100644 --- a/src/os_maild/maild.c +++ b/src/os_maild/maild.c @@ -11,6 +11,7 @@ #include "maild.h" #include "mail_list.h" #ifdef USE_SMTP_CURL +#include #include "os_net/os_net.h" #endif @@ -43,6 +44,7 @@ static void maild_clear_smtp_secrets(void) s_mail_cleanup->smtpserver_resolved = NULL; } } + curl_global_cleanup(); } #endif @@ -148,6 +150,9 @@ int main(int argc, char **argv) #ifdef USE_SMTP_CURL s_mail_cleanup = &mail; atexit(maild_clear_smtp_secrets); + if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) { + ErrorExit("%s: curl_global_init failed.", ARGV0); + } #endif /* Read internal options */ @@ -199,7 +204,7 @@ int main(int argc, char **argv) } #endif - if (mail.smtpserver[0] != '/') { + if (mail.smtpserver && mail.smtpserver[0] != '/') { /* chroot */ if (Privsep_Chroot(dir) < 0) { ErrorExit(CHROOT_ERROR, ARGV0, dir, errno, strerror(errno)); From 16a92d825d758059cda9e61c5261f2b8874dcbfa Mon Sep 17 00:00:00 2001 From: "Scott R. Shinn" Date: Sat, 14 Mar 2026 18:55:22 -0400 Subject: [PATCH 4/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/os_maild/maild.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/os_maild/maild.c b/src/os_maild/maild.c index 592fbd67c..857819d8d 100644 --- a/src/os_maild/maild.c +++ b/src/os_maild/maild.c @@ -148,11 +148,11 @@ int main(int argc, char **argv) } #ifdef USE_SMTP_CURL - s_mail_cleanup = &mail; - atexit(maild_clear_smtp_secrets); 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 */