X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/0d2e392e281e96d9f9f2f3dd438affe3f2563c57..HEAD:/src/src/rda.c diff --git a/src/src/rda.c b/src/src/rda.c index 201e82d8b..405e4646a 100644 --- a/src/src/rda.c +++ b/src/src/rda.c @@ -2,8 +2,10 @@ * Exim - an Internet mail transport agent * *************************************************/ +/* Copyright (c) The Exim maintainers 2020 - 2024 */ /* Copyright (c) University of Cambridge 1995 - 2018 */ /* See the file NOTICE for conditions of use and distribution. */ +/* SPDX-License-Identifier: GPL-2.0-or-later */ /* This module contains code for extracting addresses from a forwarding list (from an alias or forward file) or by running the filter interpreter. It may do @@ -41,7 +43,7 @@ Returns: FILTER_EXIM if it starts with "# Exim filter" static BOOL match_tag(const uschar *s, const uschar *tag) { -for (; *tag != 0; s++, tag++) +for (; *tag; s++, tag++) if (*tag == ' ') { while (*s == ' ' || *s == '\t') s++; @@ -59,10 +61,10 @@ tags for other types of filter. */ int rda_is_filter(const uschar *s) { -while (isspace(*s)) s++; /* Skips initial blank lines */ -if (match_tag(s, CUS"# exim filter")) return FILTER_EXIM; - else if (match_tag(s, CUS"# sieve filter")) return FILTER_SIEVE; - else return FILTER_FORWARD; +Uskip_whitespace(&s); /* Skips initial blank lines */ +if (match_tag(s, CUS"# exim filter")) return FILTER_EXIM; +else if (match_tag(s, CUS"# sieve filter")) return FILTER_SIEVE; +else return FILTER_FORWARD; } @@ -165,7 +167,7 @@ Returns: pointer to string in store; NULL on error */ static uschar * -rda_get_file_contents(redirect_block *rdata, int options, uschar **error, +rda_get_file_contents(const redirect_block *rdata, int options, uschar **error, int *yield) { FILE *fwd; @@ -175,6 +177,17 @@ BOOL uid_ok = !rdata->check_owner; BOOL gid_ok = !rdata->check_group; struct stat statbuf; +/* Reading a file is a form of expansion; we wish to deny attackers the +capability to specify the file name. */ + +if (is_tainted(filename)) + { + *error = string_sprintf("Tainted name '%s' for file read not permitted\n", + filename); + *yield = FF_ERROR; + return NULL; + } + /* Attempt to open the file. If it appears not to exist, check up on the containing directory by statting it. If the directory does not exist, we treat this situation as an error (which will cause delivery to defer); otherwise we @@ -195,22 +208,22 @@ if (!(fwd = Ufopen(filename, "rb"))) switch(errno) return NULL; case ENOTDIR: /* Something on the path isn't a directory */ - if ((options & RDO_ENOTDIR) == 0) goto DEFAULT_ERROR; + if (!(options & RDO_ENOTDIR)) goto DEFAULT_ERROR; DEBUG(D_route) debug_printf("non-directory on path %s: file assumed not to " "exist\n", filename); *yield = FF_NONEXIST; return NULL; case EACCES: /* Permission denied */ - if ((options & RDO_EACCES) == 0) goto DEFAULT_ERROR; + if (!(options & RDO_EACCES)) goto DEFAULT_ERROR; DEBUG(D_route) debug_printf("permission denied for %s: file assumed not to " "exist\n", filename); *yield = FF_NONEXIST; return NULL; - DEFAULT_ERROR: +DEFAULT_ERROR: default: - *error = string_open_failed(errno, "%s", filename); + *error = string_open_failed("%s", filename); *yield = FF_ERROR; return NULL; } @@ -272,7 +285,7 @@ if (statbuf.st_size > MAX_FILTER_SIZE) /* Read the file in one go in order to minimize the time we have it open. */ -filebuf = store_get(statbuf.st_size + 1, is_tainted(filename)); +filebuf = store_get(statbuf.st_size + 1, filename); if (fread(filebuf, 1, statbuf.st_size, fwd) != statbuf.st_size) { @@ -309,10 +322,7 @@ Arguments: rdata the redirection block options the options bits include_directory restrain to this directory - sieve_vacation_directory passed to sieve_interpret - sieve_enotify_mailto_owner passed to sieve_interpret - sieve_useraddress passed to sieve_interpret - sieve_subaddress passed to sieve_interpret + sieve passed to sieve_interpret generated where to hang generated addresses error for error messages eblockp for details of skipped syntax errors @@ -327,13 +337,12 @@ Returns: a suitable return for rda_interpret() */ static int -rda_extract(redirect_block *rdata, int options, uschar *include_directory, - uschar *sieve_vacation_directory, uschar *sieve_enotify_mailto_owner, - uschar *sieve_useraddress, uschar *sieve_subaddress, - address_item **generated, uschar **error, error_block **eblockp, - int *filtertype) +rda_extract(const redirect_block * rdata, int options, + const uschar * include_directory, const sieve_block * sieve, + address_item ** generated, uschar ** error, + error_block ** eblockp, int * filtertype) { -uschar *data; +const uschar * data; if (rdata->isfile) { @@ -361,7 +370,7 @@ if (*filtertype != FILTER_FORWARD) /* RDO_FILTER is an "allow" bit */ - if ((options & RDO_FILTER) == 0) + if (!(options & RDO_FILTER)) { *error = US"filtering not enabled"; return FF_ERROR; @@ -374,23 +383,40 @@ if (*filtertype != FILTER_FORWARD) if (*filtertype == FILTER_EXIM) { - if ((options & RDO_EXIM_FILTER) != 0) + const misc_module_info * mi; + typedef int (*fn_t)(const uschar *, int, address_item **, uschar **); + + if (options & RDO_EXIM_FILTER) { *error = US"Exim filtering not enabled"; return FF_ERROR; } - frc = filter_interpret(data, options, generated, error); + if (!(mi = misc_mod_find(US"exim_filter", NULL))) + { + *error = US"Exim-filtering not available"; + return FF_ERROR; + } + frc = (((fn_t *) mi->functions)[EXIM_INTERPRET]) + (data, options, generated, error); } else { - if ((options & RDO_SIEVE_FILTER) != 0) + const misc_module_info * mi; + typedef int (*fn_t)(const uschar *, int, const sieve_block *, + address_item **, uschar **); + + if (options & RDO_SIEVE_FILTER) { *error = US"Sieve filtering not enabled"; return FF_ERROR; } - frc = sieve_interpret(data, options, sieve_vacation_directory, - sieve_enotify_mailto_owner, sieve_useraddress, sieve_subaddress, - generated, error); + if (!(mi = misc_mod_find(US"sieve_filter", NULL))) + { + *error = US"Sieve filtering not available"; + return FF_ERROR; + } + frc = (((fn_t *) mi->functions)[SIEVE_INTERPRET]) + (data, options, sieve, generated, error); } expand_forbid = old_expand_forbid; @@ -430,9 +456,9 @@ Returns: -1 on error, else 0 static int rda_write_string(int fd, const uschar *s) { -int len = (s == NULL)? 0 : Ustrlen(s) + 1; +int len = s ? Ustrlen(s) + 1 : 0; return ( write(fd, &len, sizeof(int)) != sizeof(int) - || (s != NULL && write(fd, s, len) != len) + || (s && write(fd, s, len) != len) ) ? -1 : 0; } @@ -453,7 +479,7 @@ Returns: FALSE if data missing */ static BOOL -rda_read_string(int fd, uschar **sp) +rda_read_string(int fd, uschar ** sp) { int len; @@ -464,7 +490,7 @@ else /* We know we have enough memory so disable the error on "len" */ /* coverity[tainted_data] */ /* We trust the data source, so untainted */ - if (read(fd, *sp = store_get(len, FALSE), len) != len) return FALSE; + if (read(fd, *sp = store_get(len, GET_UNTAINTED), len) != len) return FALSE; return TRUE; } @@ -500,10 +526,7 @@ Arguments: options options to pass to the extraction functions, plus ENOTDIR and EACCES handling bits include_directory restrain :include: to this directory - sieve_vacation_directory directory passed to sieve_interpret - sieve_enotify_mailto_owner passed to sieve_interpret - sieve_useraddress passed to sieve_interpret - sieve_subaddress passed to sieve_interpret + sieve passed to sieve_interpret ugid uid/gid to run under - if NULL, no change generated where to hang generated addresses, initially NULL error pointer for error message @@ -529,11 +552,10 @@ Returns: values from extraction function, or FF_NONEXIST: */ int -rda_interpret(redirect_block *rdata, int options, uschar *include_directory, - uschar *sieve_vacation_directory, uschar *sieve_enotify_mailto_owner, - uschar *sieve_useraddress, uschar *sieve_subaddress, ugid_block *ugid, - address_item **generated, uschar **error, error_block **eblockp, - int *filtertype, uschar *rname) +rda_interpret(redirect_block * rdata, int options, + const uschar * include_directory, const sieve_block * sieve, + const ugid_block * ugid, address_item ** generated, + uschar ** error, error_block ** eblockp, int * filtertype, const uschar * rname) { int fd, rc, pfd[2]; int yield, status; @@ -571,15 +593,13 @@ with #Exim filter or #Sieve filter, and does not contain :include:, do all the work in this process. Note that for a system filter, we always have a file, so the work is done in this process only if no user is supplied. */ -if (!ugid->uid_set || /* Either there's no uid, or */ - (!rdata->isfile && /* We've got the data, and */ - rda_is_filter(data) == FILTER_FORWARD && /* It's not a filter script, */ - Ustrstr(data, ":include:") == NULL)) /* and there's no :include: */ - { - return rda_extract(rdata, options, include_directory, - sieve_vacation_directory, sieve_enotify_mailto_owner, sieve_useraddress, - sieve_subaddress, generated, error, eblockp, filtertype); - } +if ( !ugid->uid_set /* Either there's no uid, or */ + || ( !rdata->isfile /* We've got the data, and */ + && rda_is_filter(data) == FILTER_FORWARD /* It's not a filter script, */ + && Ustrstr(data, ":include:") == NULL /* and there's no :include: */ + ) ) + return rda_extract(rdata, options, include_directory, sieve, + generated, error, eblockp, filtertype); /* We need to run the processing code in a sub-process. However, if we can determine the non-existence of a file first, we can decline without having to @@ -605,12 +625,17 @@ with the parent process. */ oldsignal = signal(SIGCHLD, SIG_DFL); search_tidyup(); -if ((pid = fork()) == 0) +if ((pid = exim_fork(US"router-interpret")) == 0) { header_line *waslast = header_last; /* Save last header */ + int fd_flags = -1; fd = pfd[pipe_write]; (void)close(pfd[pipe_read]); + + if ((fd_flags = fcntl(fd, F_GETFD)) == -1) goto bad; + if (fcntl(fd, F_SETFD, fd_flags | FD_CLOEXEC) == -1) goto bad; + exim_setugid(ugid->uid, ugid->gid, FALSE, rname); /* Addresses can get rewritten in filters; if we are not root or the exim @@ -626,9 +651,8 @@ if ((pid = fork()) == 0) /* Now do the business */ - yield = rda_extract(rdata, options, include_directory, - sieve_vacation_directory, sieve_enotify_mailto_owner, sieve_useraddress, - sieve_subaddress, generated, error, eblockp, filtertype); + yield = rda_extract(rdata, options, include_directory, sieve, + generated, error, eblockp, filtertype); /* Pass back whether it was a filter, and the return code and any overall error text via the pipe. */ @@ -758,7 +782,7 @@ if ((pid = fork()) == 0) out: (void)close(fd); search_tidyup(); - exim_underbar_exit(0); + exim_underbar_exit(EXIT_SUCCESS); bad: DEBUG(D_rewrite) debug_printf("rda_interpret: failed write to pipe\n"); @@ -793,7 +817,7 @@ if (eblockp) uschar *s; if (!rda_read_string(fd, &s)) goto DISASTER; if (!s) break; - e = store_get(sizeof(error_block), FALSE); + e = store_get(sizeof(error_block), GET_UNTAINTED); e->next = NULL; e->text1 = s; if (!rda_read_string(fd, &s)) goto DISASTER; @@ -850,9 +874,9 @@ if (yield == FF_DELIVERED || yield == FF_NOTDELIVERED || for (;;) { int i, reply_options; - address_item *addr; - uschar *recipient; - uschar *expandn[EXPAND_MAXN + 2]; + address_item * addr; + uschar * recipient, * s; + uschar * expandn[EXPAND_MAXN + 2]; /* First string is the address; NULL => end of addresses */ @@ -869,10 +893,11 @@ if (yield == FF_DELIVERED || yield == FF_NOTDELIVERED || if ( read(fd, &addr->mode, sizeof(addr->mode)) != sizeof(addr->mode) || read(fd, &addr->flags, sizeof(addr->flags)) != sizeof(addr->flags) - || !rda_read_string(fd, &addr->prop.errors_address) + || !rda_read_string(fd, &s) || read(fd, &i, sizeof(i)) != sizeof(i) ) goto DISASTER; + addr->prop.errors_address = s; addr->prop.ignore_error = (i != 0); /* Next comes a possible setting for $thisaddress and any numerical @@ -892,7 +917,7 @@ if (yield == FF_DELIVERED || yield == FF_NOTDELIVERED || if (i > 0) { - addr->pipe_expandn = store_get((i+1) * sizeof(uschar *), FALSE); + addr->pipe_expandn = store_get((i+1) * sizeof(uschar *), GET_UNTAINTED); addr->pipe_expandn[i] = NULL; while (--i >= 0) addr->pipe_expandn[i] = expandn[i]; } @@ -902,7 +927,7 @@ if (yield == FF_DELIVERED || yield == FF_NOTDELIVERED || if (read(fd, &reply_options, sizeof(int)) != sizeof(int)) goto DISASTER; if ((reply_options & REPLY_EXISTS) != 0) { - addr->reply = store_get(sizeof(reply_item), FALSE); + addr->reply = store_get(sizeof(reply_item), GET_UNTAINTED); addr->reply->file_expand = (reply_options & REPLY_EXPAND) != 0; addr->reply->return_message = (reply_options & REPLY_RETURN) != 0; @@ -911,17 +936,17 @@ if (yield == FF_DELIVERED || yield == FF_NOTDELIVERED || sizeof(int) || read(fd,&(addr->reply->once_repeat),sizeof(time_t)) != sizeof(time_t) || - !rda_read_string(fd, &(addr->reply->to)) || - !rda_read_string(fd, &(addr->reply->cc)) || - !rda_read_string(fd, &(addr->reply->bcc)) || - !rda_read_string(fd, &(addr->reply->from)) || - !rda_read_string(fd, &(addr->reply->reply_to)) || - !rda_read_string(fd, &(addr->reply->subject)) || - !rda_read_string(fd, &(addr->reply->headers)) || - !rda_read_string(fd, &(addr->reply->text)) || - !rda_read_string(fd, &(addr->reply->file)) || - !rda_read_string(fd, &(addr->reply->logfile)) || - !rda_read_string(fd, &(addr->reply->oncelog))) + !rda_read_string(fd, &addr->reply->to) || + !rda_read_string(fd, &addr->reply->cc) || + !rda_read_string(fd, &addr->reply->bcc) || + !rda_read_string(fd, &addr->reply->from) || + !rda_read_string(fd, &addr->reply->reply_to) || + !rda_read_string(fd, &addr->reply->subject) || + !rda_read_string(fd, &addr->reply->headers) || + !rda_read_string(fd, &addr->reply->text) || + !rda_read_string(fd, &addr->reply->file) || + !rda_read_string(fd, &addr->reply->logfile) || + !rda_read_string(fd, &addr->reply->oncelog)) goto DISASTER; } } @@ -932,13 +957,11 @@ reading end of the pipe, and we are done. */ WAIT_EXIT: while ((rc = wait(&status)) != pid) - { if (rc < 0 && errno == ECHILD) /* Process has vanished */ { log_write(0, LOG_MAIN, "redirection process %d vanished unexpectedly", pid); goto FINAL_EXIT; } - } DEBUG(D_route) debug_printf("rda_interpret: subprocess yield=%d error=%s\n", yield, *error); @@ -948,16 +971,14 @@ if (had_disaster) *error = string_sprintf("internal problem in %s: failure to transfer " "data from subprocess: status=%04x%s%s%s", rname, status, readerror, - (*error == NULL)? US"" : US": error=", - (*error == NULL)? US"" : *error); + *error ? US": error=" : US"", + *error ? *error : US""); log_write(0, LOG_MAIN|LOG_PANIC, "%s", *error); } else if (status != 0) - { log_write(0, LOG_MAIN|LOG_PANIC, "internal problem in %s: unexpected status " "%04x from redirect subprocess (but data correctly received)", rname, status); - } FINAL_EXIT: (void)close(fd);