diff options
Diffstat (limited to 'src/postdrop/postdrop.c')
-rw-r--r-- | src/postdrop/postdrop.c | 628 |
1 files changed, 628 insertions, 0 deletions
diff --git a/src/postdrop/postdrop.c b/src/postdrop/postdrop.c new file mode 100644 index 0000000..e9335e9 --- /dev/null +++ b/src/postdrop/postdrop.c @@ -0,0 +1,628 @@ +/*++ +/* NAME +/* postdrop 1 +/* SUMMARY +/* Postfix mail posting utility +/* SYNOPSIS +/* \fBpostdrop\fR [\fB-rv\fR] [\fB-c \fIconfig_dir\fR] +/* DESCRIPTION +/* The \fBpostdrop\fR(1) command creates a file in the \fBmaildrop\fR +/* directory and copies its standard input to the file. +/* +/* Options: +/* .IP "\fB-c \fIconfig_dir\fR" +/* The \fBmain.cf\fR configuration file is in the named directory +/* instead of the default configuration directory. See also the +/* MAIL_CONFIG environment setting below. +/* .IP \fB-r\fR +/* Use a Postfix-internal protocol for reading the message from +/* standard input, and for reporting status information on standard +/* output. This is currently the only supported method. +/* .IP \fB-v\fR +/* Enable verbose logging for debugging purposes. Multiple \fB-v\fR +/* options make the software increasingly verbose. As of Postfix 2.3, +/* this option is available for the super-user only. +/* SECURITY +/* .ad +/* .fi +/* The command is designed to run with set-group ID privileges, so +/* that it can write to the \fBmaildrop\fR queue directory and so that +/* it can connect to Postfix daemon processes. +/* DIAGNOSTICS +/* Fatal errors: malformed input, I/O error, out of memory. Problems +/* are logged to \fBsyslogd\fR(8) or \fBpostlogd\fR(8) and to +/* the standard error stream. +/* When the input is incomplete, or when the process receives a HUP, +/* INT, QUIT or TERM signal, the queue file is deleted. +/* ENVIRONMENT +/* .ad +/* .fi +/* .IP MAIL_CONFIG +/* Directory with the \fBmain.cf\fR file. In order to avoid exploitation +/* of set-group ID privileges, a non-standard directory is allowed only +/* if: +/* .RS +/* .IP \(bu +/* The name is listed in the standard \fBmain.cf\fR file with the +/* \fBalternate_config_directories\fR configuration parameter. +/* .IP \(bu +/* The command is invoked by the super-user. +/* .RE +/* CONFIGURATION PARAMETERS +/* .ad +/* .fi +/* The following \fBmain.cf\fR parameters are especially relevant to +/* this program. +/* The text below provides only a parameter summary. See +/* \fBpostconf\fR(5) for more details including examples. +/* .IP "\fBalternate_config_directories (empty)\fR" +/* A list of non-default Postfix configuration directories that may +/* be specified with "-c config_directory" on the command line (in the +/* case of \fBsendmail\fR(1), with the "-C" option), or via the MAIL_CONFIG +/* environment parameter. +/* .IP "\fBconfig_directory (see 'postconf -d' output)\fR" +/* The default location of the Postfix main.cf and master.cf +/* configuration files. +/* .IP "\fBimport_environment (see 'postconf -d' output)\fR" +/* The list of environment parameters that a privileged Postfix +/* process will import from a non-Postfix parent process, or name=value +/* environment overrides. +/* .IP "\fBqueue_directory (see 'postconf -d' output)\fR" +/* The location of the Postfix top-level queue directory. +/* .IP "\fBsyslog_facility (mail)\fR" +/* The syslog facility of Postfix logging. +/* .IP "\fBsyslog_name (see 'postconf -d' output)\fR" +/* A prefix that is prepended to the process name in syslog +/* records, so that, for example, "smtpd" becomes "prefix/smtpd". +/* .IP "\fBtrigger_timeout (10s)\fR" +/* The time limit for sending a trigger to a Postfix daemon (for +/* example, the \fBpickup\fR(8) or \fBqmgr\fR(8) daemon). +/* .PP +/* Available in Postfix version 2.2 and later: +/* .IP "\fBauthorized_submit_users (static:anyone)\fR" +/* List of users who are authorized to submit mail with the \fBsendmail\fR(1) +/* command (and with the privileged \fBpostdrop\fR(1) helper command). +/* .PP +/* Available in Postfix version 3.6 and later: +/* .IP "\fBlocal_login_sender_maps (static:*)\fR" +/* A list of lookup tables that are searched by the UNIX login name, +/* and that return a list of allowed envelope sender patterns separated +/* by space or comma. +/* .IP "\fBempty_address_local_login_sender_maps_lookup_key (<>)\fR" +/* The lookup key to be used in local_login_sender_maps tables, instead +/* of the null sender address. +/* .IP "\fBrecipient_delimiter (empty)\fR" +/* The set of characters that can separate an email address +/* localpart, user name, or a .forward file name from its extension. +/* FILES +/* /var/spool/postfix/maildrop, maildrop queue +/* SEE ALSO +/* sendmail(1), compatibility interface +/* postconf(5), configuration parameters +/* postlogd(8), Postfix logging +/* syslogd(8), system logging +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <sys/stat.h> +#include <unistd.h> +#include <stdlib.h> +#include <stdio.h> /* remove() */ +#include <string.h> +#include <stdlib.h> +#include <signal.h> +#include <errno.h> +#include <warn_stat.h> + +/* Utility library. */ + +#include <msg.h> +#include <mymalloc.h> +#include <vstream.h> +#include <vstring.h> +#include <msg_vstream.h> +#include <argv.h> +#include <iostuff.h> +#include <stringops.h> +#include <mypwd.h> + +/* Global library. */ + +#include <mail_proto.h> +#include <mail_queue.h> +#include <mail_params.h> +#include <mail_version.h> +#include <mail_conf.h> +#include <mail_task.h> +#include <clean_env.h> +#include <mail_stream.h> +#include <cleanup_user.h> +#include <record.h> +#include <rec_type.h> +#include <mail_dict.h> +#include <user_acl.h> +#include <rec_attr_map.h> +#include <mail_parm_split.h> +#include <maillog_client.h> +#include <login_sender_match.h> + +/* Application-specific. */ + + /* + * WARNING WARNING WARNING + * + * This software is designed to run set-gid. In order to avoid exploitation of + * privilege, this software should not run any external commands, nor should + * it take any information from the user unless that information can be + * properly sanitized. To get an idea of how much information a process can + * inherit from a potentially hostile user, examine all the members of the + * process structure (typically, in /usr/include/sys/proc.h): the current + * directory, open files, timers, signals, environment, command line, umask, + * and so on. + */ + + /* + * Local mail submission access list. + */ +char *var_submit_acl; +char *var_local_login_snd_maps; +char *var_null_local_login_snd_maps_key; + +static const CONFIG_STR_TABLE str_table[] = { + VAR_SUBMIT_ACL, DEF_SUBMIT_ACL, &var_submit_acl, 0, 0, + VAR_LOCAL_LOGIN_SND_MAPS, DEF_LOCAL_LOGIN_SND_MAPS, &var_local_login_snd_maps, 0, 0, + VAR_NULL_LOCAL_LOGIN_SND_MAPS_KEY, DEF_NULL_LOCAL_LOGIN_SND_MAPS_KEY, &var_null_local_login_snd_maps_key, 0, 0, + 0, +}; + + /* + * Queue file name. Global, so that the cleanup routine can find it when + * called by the run-time error handler. + */ +static char *postdrop_path; + +/* postdrop_sig - catch signal and clean up */ + +static void postdrop_sig(int sig) +{ + + /* + * This is the fatal error handler. Don't try to do anything fancy. + * + * To avoid privilege escalation in a set-gid program, Postfix logging + * functions must not be called from a user-triggered signal handler, + * because Postfix logging functions may allocate memory on the fly (as + * does the syslog() library function), and the memory allocator is not + * reentrant. + * + * Assume atomic signal() updates, even when emulated with sigaction(). We + * use the in-kernel SIGINT handler address as an atomic variable to + * prevent nested postdrop_sig() calls. For this reason, main() must + * configure postdrop_sig() as SIGINT handler before other signal + * handlers are allowed to invoke postdrop_sig(). + */ + if (signal(SIGINT, SIG_IGN) != SIG_IGN) { + (void) signal(SIGQUIT, SIG_IGN); + (void) signal(SIGTERM, SIG_IGN); + (void) signal(SIGHUP, SIG_IGN); + if (postdrop_path) { + (void) remove(postdrop_path); + postdrop_path = 0; + } + /* Future proofing. If you need exit() here then you broke Postfix. */ + if (sig) + _exit(sig); + } +} + +/* postdrop_cleanup - callback for the runtime error handler */ + +static void postdrop_cleanup(void) +{ + postdrop_sig(0); +} + +/* check_login_sender_acl - check if a user is authorized to use this sender */ + +static int check_login_sender_acl(uid_t uid, VSTRING *sender_buf, + VSTRING *reason) +{ + const char myname[] = "check_login_sender_acl"; + struct mypasswd *user_info; + char *user_name; + VSTRING *user_name_buf = 0; + LOGIN_SENDER_MATCH *lsm; + int res; + + /* + * Sanity checks. + */ + if (vstring_memchr(sender_buf, '\0') != 0) { + vstring_sprintf(reason, "NUL in FROM record"); + return (CLEANUP_STAT_BAD); + } + + /* + * Optimization. + */ +#ifndef SNAPSHOT + if (strcmp(var_local_login_snd_maps, DEF_LOCAL_LOGIN_SND_MAPS) == 0) + return (CLEANUP_STAT_OK); +#endif + + /* + * Get the username. + */ + if ((user_info = mypwuid(uid)) != 0) { + user_name = user_info->pw_name; + } else { + user_name_buf = vstring_alloc(10); + vstring_sprintf(user_name_buf, "uid:%ld", (long) uid); + user_name = vstring_str(user_name_buf); + } + + + /* + * Apply the a login-sender matcher. TODO: add DICT flags. + */ + lsm = login_sender_create(VAR_LOCAL_LOGIN_SND_MAPS, + var_local_login_snd_maps, + var_rcpt_delim, + var_null_local_login_snd_maps_key, "*"); + res = login_sender_match(lsm, user_name, vstring_str(sender_buf)); + login_sender_free(lsm); + if (user_name_buf) + vstring_free(user_name_buf); + switch (res) { + case LSM_STAT_FOUND: + return (CLEANUP_STAT_OK); + case LSM_STAT_NOTFOUND: + vstring_sprintf(reason, "not authorized to use sender='%s'", + vstring_str(sender_buf)); + return (CLEANUP_STAT_NOPERM); + case LSM_STAT_RETRY: + case LSM_STAT_CONFIG: + vstring_sprintf(reason, "%s table lookup error for '%s'", + VAR_LOCAL_LOGIN_SND_MAPS, var_local_login_snd_maps); + return (CLEANUP_STAT_WRITE); + default: + msg_panic("%s: bad login_sender_match() result: %d", myname, res); + } +} + +MAIL_VERSION_STAMP_DECLARE; + +/* main - the main program */ + +int main(int argc, char **argv) +{ + struct stat st; + int fd; + int c; + VSTRING *buf; + int status = CLEANUP_STAT_OK; + VSTRING *reason = vstring_alloc(100); + MAIL_STREAM *dst; + int rec_type; + static char *segment_info[] = { + REC_TYPE_POST_ENVELOPE, REC_TYPE_POST_CONTENT, REC_TYPE_POST_EXTRACT, "" + }; + char **expected; + uid_t uid = getuid(); + ARGV *import_env; + const char *error_text; + char *attr_name; + char *attr_value; + const char *errstr; + char *junk; + struct timeval start; + int saved_errno; + int from_count = 0; + int rcpt_count = 0; + int validate_input = 1; + + /* + * Fingerprint executables and core dumps. + */ + MAIL_VERSION_STAMP_ALLOCATE; + + /* + * Be consistent with file permissions. + */ + umask(022); + + /* + * To minimize confusion, make sure that the standard file descriptors + * are open before opening anything else. XXX Work around for 44BSD where + * fstat can return EBADF on an open file descriptor. + */ + for (fd = 0; fd < 3; fd++) + if (fstat(fd, &st) == -1 + && (close(fd), open("/dev/null", O_RDWR, 0)) != fd) + msg_fatal("open /dev/null: %m"); + + /* + * Set up logging. Censor the process name: it is provided by the user. + */ + argv[0] = "postdrop"; + msg_vstream_init(argv[0], VSTREAM_ERR); + maillog_client_init(mail_task("postdrop"), MAILLOG_CLIENT_FLAG_NONE); + set_mail_conf_str(VAR_PROCNAME, var_procname = mystrdup(argv[0])); + + /* + * Check the Postfix library version as soon as we enable logging. + */ + MAIL_VERSION_CHECK; + + /* + * Parse JCL. This program is set-gid and must sanitize all command-line + * arguments. The configuration directory argument is validated by the + * mail configuration read routine. Don't do complex things until we have + * completed initializations. + */ + while ((c = GETOPT(argc, argv, "c:rv")) > 0) { + switch (c) { + case 'c': + if (setenv(CONF_ENV_PATH, optarg, 1) < 0) + msg_fatal("out of memory"); + break; + case 'r': /* forward compatibility */ + break; + case 'v': + if (geteuid() == 0) + msg_verbose++; + break; + default: + msg_fatal("usage: %s [-c config_dir] [-v]", argv[0]); + } + } + + /* + * Read the global configuration file and extract configuration + * information. + */ + mail_conf_read(); + /* Re-evaluate mail_task() after reading main.cf. */ + maillog_client_init(mail_task("postdrop"), MAILLOG_CLIENT_FLAG_NONE); + get_mail_conf_str_table(str_table); + + /* + * Stop run-away process accidents by limiting the queue file size. This + * is not a defense against DOS attack. + */ + if (ENFORCING_SIZE_LIMIT(var_message_limit) + && get_file_limit() > var_message_limit) + set_file_limit((off_t) var_message_limit); + + /* + * This program is installed with setgid privileges. Strip the process + * environment so that we don't have to trust the C library. + */ + import_env = mail_parm_split(VAR_IMPORT_ENVIRON, var_import_environ); + clean_env(import_env->argv); + argv_free(import_env); + + if (chdir(var_queue_dir)) + msg_fatal("chdir %s: %m", var_queue_dir); + if (msg_verbose) + msg_info("chdir %s", var_queue_dir); + + /* + * Set up signal handlers and a runtime error handler so that we can + * clean up incomplete output. + * + * postdrop_sig() uses the in-kernel SIGINT handler address as an atomic + * variable to prevent nested postdrop_sig() calls. For this reason, the + * SIGINT handler must be configured before other signal handlers are + * allowed to invoke postdrop_sig(). + */ + signal(SIGPIPE, SIG_IGN); + signal(SIGXFSZ, SIG_IGN); + + signal(SIGINT, postdrop_sig); + signal(SIGQUIT, postdrop_sig); + if (signal(SIGTERM, SIG_IGN) == SIG_DFL) + signal(SIGTERM, postdrop_sig); + if (signal(SIGHUP, SIG_IGN) == SIG_DFL) + signal(SIGHUP, postdrop_sig); + msg_cleanup(postdrop_cleanup); + + /* End of initializations. */ + + /* + * Mail submission access control. Should this be in the user-land gate, + * or in the daemon process? + */ + mail_dict_init(); + if ((errstr = check_user_acl_byuid(VAR_SUBMIT_ACL, var_submit_acl, + uid)) != 0) + msg_fatal("User %s(%ld) is not allowed to submit mail", + errstr, (long) uid); + + /* + * Don't trust the caller's time information. + */ + GETTIMEOFDAY(&start); + + /* + * Create queue file. mail_stream_file() never fails. Send the queue ID + * to the caller. Stash away a copy of the queue file name so we can + * clean up in case of a fatal error or an interrupt. + */ + dst = mail_stream_file(MAIL_QUEUE_MAILDROP, MAIL_CLASS_PUBLIC, + var_pickup_service, 0444); + attr_print(VSTREAM_OUT, ATTR_FLAG_NONE, + SEND_ATTR_STR(MAIL_ATTR_PROTO, MAIL_ATTR_PROTO_POSTDROP), + SEND_ATTR_STR(MAIL_ATTR_QUEUEID, dst->id), + ATTR_TYPE_END); + vstream_fflush(VSTREAM_OUT); + postdrop_path = mystrdup(VSTREAM_PATH(dst->stream)); + + /* + * Copy stdin to file. The format is checked so that we can recognize + * incomplete input and cancel the operation. With the sanity checks + * applied here, the pickup daemon could skip format checks and pass a + * file descriptor to the cleanup daemon. These are by no means all + * sanity checks - the cleanup service and queue manager services will + * reject messages that lack required information. + * + * If something goes wrong, slurp up the input before responding to the + * client, otherwise the client will give up after detecting SIGPIPE. + * + * Allow attribute records if the attribute specifies the MIME body type + * (sendmail -B). + */ + vstream_control(VSTREAM_IN, CA_VSTREAM_CTL_PATH("stdin"), CA_VSTREAM_CTL_END); + buf = vstring_alloc(100); + expected = segment_info; + /* Override time information from the untrusted caller. */ + rec_fprintf(dst->stream, REC_TYPE_TIME, REC_TYPE_TIME_FORMAT, + REC_TYPE_TIME_ARG(start)); + for (;;) { + /* Don't allow PTR records. */ + rec_type = rec_get_raw(VSTREAM_IN, buf, var_line_limit, REC_FLAG_NONE); + if (rec_type == REC_TYPE_EOF) { /* request canceled */ + mail_stream_cleanup(dst); + if (remove(postdrop_path)) + msg_warn("uid=%ld: remove %s: %m", (long) uid, postdrop_path); + else if (msg_verbose) + msg_info("remove %s", postdrop_path); + myfree(postdrop_path); + postdrop_path = 0; + exit(0); + } + if (rec_type == REC_TYPE_ERROR) + msg_fatal("uid=%ld: malformed input", (long) uid); + if (strchr(*expected, rec_type) == 0) + msg_fatal("uid=%ld: unexpected record type: %d", (long) uid, rec_type); + if (rec_type == **expected) + expected++; + /* Override time information from the untrusted caller. */ + if (rec_type == REC_TYPE_TIME) + continue; + /* Check these at submission time instead of pickup time. */ + if (rec_type == REC_TYPE_FROM) { + status |= check_login_sender_acl(uid, buf, reason); + from_count++; + } + if (rec_type == REC_TYPE_RCPT) + rcpt_count++; + /* Limit the attribute types that users may specify. */ + if (rec_type == REC_TYPE_ATTR) { + if ((error_text = split_nameval(vstring_str(buf), &attr_name, + &attr_value)) != 0) { + msg_warn("uid=%ld: ignoring malformed record: %s: %.200s", + (long) uid, error_text, vstring_str(buf)); + continue; + } +#define STREQ(x,y) (strcmp(x,y) == 0) + + if ((STREQ(attr_name, MAIL_ATTR_ENCODING) + && (STREQ(attr_value, MAIL_ATTR_ENC_7BIT) + || STREQ(attr_value, MAIL_ATTR_ENC_8BIT) + || STREQ(attr_value, MAIL_ATTR_ENC_NONE))) + || STREQ(attr_name, MAIL_ATTR_DSN_ENVID) + || STREQ(attr_name, MAIL_ATTR_DSN_NOTIFY) + || rec_attr_map(attr_name) + || (STREQ(attr_name, MAIL_ATTR_RWR_CONTEXT) + && (STREQ(attr_value, MAIL_ATTR_RWR_LOCAL) + || STREQ(attr_value, MAIL_ATTR_RWR_REMOTE))) + || STREQ(attr_name, MAIL_ATTR_TRACE_FLAGS)) { /* XXX */ + rec_fprintf(dst->stream, REC_TYPE_ATTR, "%s=%s", + attr_name, attr_value); + } else { + msg_warn("uid=%ld: ignoring attribute record: %.200s=%.200s", + (long) uid, attr_name, attr_value); + } + continue; + } + if (status != CLEANUP_STAT_OK + || REC_PUT_BUF(dst->stream, rec_type, buf) < 0) { + /* rec_get() errors must not clobber errno. */ + saved_errno = errno; + while ((rec_type = rec_get_raw(VSTREAM_IN, buf, var_line_limit, + REC_FLAG_NONE)) != REC_TYPE_END + && rec_type != REC_TYPE_EOF) + if (rec_type == REC_TYPE_ERROR) + msg_fatal("uid=%ld: malformed input", (long) uid); + validate_input = 0; + errno = saved_errno; + break; + } + if (rec_type == REC_TYPE_END) + break; + } + vstring_free(buf); + + /* + * As of Postfix 2.7 the pickup daemon discards mail without recipients. + * Such mail may enter the maildrop queue when "postsuper -r" is invoked + * before the queue manager deletes an already delivered message. Looking + * at file ownership is not a good way to make decisions on what mail to + * discard. Instead, the pickup server now requires that new submissions + * always have at least one recipient record. + * + * The Postfix sendmail command already rejects mail without recipients. + * However, in the future postdrop may receive mail via other programs, + * so we add a redundant recipient check here for future proofing. + * + * The test for the sender address is just for consistency of error + * reporting (report at submission time instead of pickup time). Besides + * the segment terminator records, there aren't any other mandatory + * records in a Postfix submission queue file. + * + * TODO: return an informative reason for missing sender, too many senders, + * or missing recipient. + */ + if (validate_input && (from_count == 0 || rcpt_count == 0)) + status |= CLEANUP_STAT_BAD; + if (status != CLEANUP_STAT_OK) { + mail_stream_cleanup(dst); + } + + /* + * Finish the file. + */ + else if ((status = mail_stream_finish(dst, reason)) != 0) { + msg_warn("uid=%ld: %m", (long) uid); + postdrop_cleanup(); + } + + /* + * Disable deletion on fatal error before reporting success, so the file + * will not be deleted after we have taken responsibility for delivery. + */ + if (postdrop_path) { + junk = postdrop_path; + postdrop_path = 0; + myfree(junk); + } + + /* + * Send the completion status to the caller and terminate. + */ + attr_print(VSTREAM_OUT, ATTR_FLAG_NONE, + SEND_ATTR_INT(MAIL_ATTR_STATUS, status), + SEND_ATTR_STR(MAIL_ATTR_WHY, status != CLEANUP_STAT_OK + && VSTRING_LEN(reason) > 0 ? + vstring_str(reason) : ""), + ATTR_TYPE_END); + vstream_fflush(VSTREAM_OUT); + exit(status); +} |