X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/fbe8578a39505c146223ffcf2c63a5ba8bb0d9a4..cb45303cf2a8d9922702f13db42b3285c48f6aa7:/src/src/acl.c diff --git a/src/src/acl.c b/src/src/acl.c index f47259ca0..aa8699a8a 100644 --- a/src/src/acl.c +++ b/src/src/acl.c @@ -3,13 +3,14 @@ *************************************************/ /* Copyright (c) University of Cambridge 1995 - 2018 */ -/* Copyright (c) The Exim Maintainers 2020 */ +/* Copyright (c) The Exim Maintainers 2020 - 2021 */ /* See the file NOTICE for conditions of use and distribution. */ /* Code for handling Access Control Lists (ACLs) */ #include "exim.h" +#ifndef MACRO_PREDEF /* Default callout timeout */ @@ -53,6 +54,8 @@ static int msgcond[] = { [ACL_WARN] = BIT(OK) }; +#endif + /* ACL condition and modifier codes - keep in step with the table that follows. down. */ @@ -103,6 +106,7 @@ enum { ACLC_ACL, ACLC_REGEX, #endif ACLC_REMOVE_HEADER, + ACLC_SEEN, ACLC_SENDER_DOMAINS, ACLC_SENDERS, ACLC_SET, @@ -288,6 +292,7 @@ static condition_def conditions[] = { ACL_BIT_MIME | ACL_BIT_NOTSMTP | ACL_BIT_NOTSMTP_START), }, + [ACLC_SEEN] = { US"seen", TRUE, FALSE, 0 }, [ACLC_SENDER_DOMAINS] = { US"sender_domains", FALSE, FALSE, ACL_BIT_AUTH | ACL_BIT_CONNECT | ACL_BIT_HELO | @@ -338,6 +343,24 @@ static condition_def conditions[] = { }; +#ifdef MACRO_PREDEF +# include "macro_predef.h" +void +features_acl(void) +{ +for (condition_def * c = conditions; c < conditions + nelem(conditions); c++) + { + uschar buf[64], * p, * s; + int n = sprintf(CS buf, "_ACL_%s_", c->is_modifier ? "MOD" : "COND"); + for (p = buf + n, s = c->name; *s; s++) *p++ = toupper(*s); + *p = '\0'; + builtin_macro_create(buf); + } +} +#endif + + +#ifndef MACRO_PREDEF /* Return values from decode_control(); used as index so keep in step with the controls_list table that follows! */ @@ -619,6 +642,8 @@ static uschar *ratelimit_option_string[] = { static int acl_check_wargs(int, address_item *, const uschar *, uschar **, uschar **); +static acl_block * acl_current = NULL; + /************************************************* * Find control in list * @@ -783,7 +808,7 @@ while ((s = (*func)())) *error = string_sprintf("malformed ACL line \"%s\"", saveline); return NULL; } - this = store_get(sizeof(acl_block), FALSE); + this = store_get(sizeof(acl_block), GET_UNTAINTED); *lastp = this; lastp = &(this->next); this->next = NULL; @@ -830,7 +855,7 @@ while ((s = (*func)())) return NULL; } - cond = store_get(sizeof(acl_condition_block), FALSE); + cond = store_get(sizeof(acl_condition_block), GET_UNTAINTED); cond->next = NULL; cond->type = c; cond->u.negated = negated; @@ -1029,7 +1054,7 @@ for (p = q; *p; p = q) { /* The header_line struct itself is not tainted, though it points to possibly tainted data. */ - header_line * h = store_get(sizeof(header_line), FALSE); + header_line * h = store_get(sizeof(header_line), GET_UNTAINTED); h->text = hdr; h->next = NULL; h->type = newtype; @@ -1188,7 +1213,7 @@ int rc; /* Previous success */ -if (sender_host_name != NULL) return OK; +if (sender_host_name) return OK; /* Previous failure */ @@ -1318,7 +1343,7 @@ dns_scan dnss; dns_record *rr; int rc, type, yield; #define TARGET_SIZE 256 -uschar * target = store_get(TARGET_SIZE, TRUE); +uschar * target = store_get(TARGET_SIZE, GET_TAINTED); /* Work out the domain we are using for the CSA lookup. The default is the client's HELO domain. If the client has not said HELO, use its IP address @@ -1360,7 +1385,7 @@ we return from this function. */ if ((t = tree_search(csa_cache, domain))) return t->data.val; -t = store_get_perm(sizeof(tree_node) + Ustrlen(domain), is_tainted(domain)); +t = store_get_perm(sizeof(tree_node) + Ustrlen(domain), domain); Ustrcpy(t->name, domain); (void)tree_insertnode(&csa_cache, t); @@ -2562,7 +2587,7 @@ if (!dbdb) /* No Bloom filter. This basic ratelimit block is initialized below. */ HDEBUG(D_acl) debug_printf_indent("ratelimit creating new rate data block\n"); dbdb_size = sizeof(*dbd); - dbdb = store_get(dbdb_size, FALSE); /* not tainted */ + dbdb = store_get(dbdb_size, GET_UNTAINTED); } else { @@ -2576,7 +2601,7 @@ if (!dbdb) extra = (int)limit * 2 - sizeof(dbdb->bloom); if (extra < 0) extra = 0; dbdb_size = sizeof(*dbdb) + extra; - dbdb = store_get(dbdb_size, FALSE); /* not tainted */ + dbdb = store_get(dbdb_size, GET_UNTAINTED); dbdb->bloom_epoch = tv.tv_sec; dbdb->bloom_size = sizeof(dbdb->bloom) + extra; memset(dbdb->bloom, 0, dbdb->bloom_size); @@ -2796,7 +2821,7 @@ dbfn_close(dbm); /* Store the result in the tree for future reference. Take the taint status from the key for consistency even though it's unlikely we'll ever expand this. */ -t = store_get(sizeof(tree_node) + Ustrlen(key), is_tainted(key)); +t = store_get(sizeof(tree_node) + Ustrlen(key), key); t->data.ptr = dbd; Ustrcpy(t->name, key); (void)tree_insertnode(anchor, t); @@ -2815,6 +2840,143 @@ return rc; +/************************************************* +* Handle a check for previously-seen * +*************************************************/ + +/* +ACL clauses like: seen = -5m / key=$foo / readonly + +Return is true for condition-true - but the semantics +depend heavily on the actual use-case. + +Negative times test for seen-before, positive for seen-more-recently-than +(the given interval before current time). + +All are subject to history not having been cleaned from the DB. + +Default for seen-before is to create if not present, and to +update if older than 10d (with the seen-test time). +Default for seen-since is to always create or update. + +Options: + key=value. Default key is $sender_host_address + readonly + write + refresh=: update an existing DB entry older than given + amount. Default refresh lacking this option is 10d. + The update sets the record timestamp to the seen-test time. + +XXX do we need separate nocreate, noupdate controls? + +Arguments: + arg the option string for seen= + where ACL_WHERE_xxxx indicating which ACL this is + log_msgptr for error messages + +Returns: OK - Condition is true + FAIL - Condition is false + DEFER - Problem opening history database + ERROR - Syntax error in options +*/ + +static int +acl_seen(const uschar * arg, int where, uschar ** log_msgptr) +{ +enum { SEEN_DEFAULT, SEEN_READONLY, SEEN_WRITE }; + +const uschar * list = arg; +int slash = '/', interval, mode = SEEN_DEFAULT, yield = FAIL; +BOOL before; +int refresh = 10 * 24 * 60 * 60; /* 10 days */ +const uschar * ele, * key = sender_host_address; +open_db dbblock, * dbm; +dbdata_seen * dbd; +time_t now; + +/* Parse the first element, the time-relation. */ + +if (!(ele = string_nextinlist(&list, &slash, NULL, 0))) + goto badparse; +if ((before = *ele == '-')) + ele++; +if ((interval = readconf_readtime(ele, 0, FALSE)) < 0) + goto badparse; + +/* Remaining elements are options */ + +while ((ele = string_nextinlist(&list, &slash, NULL, 0))) + if (Ustrncmp(ele, "key=", 4) == 0) + key = ele + 4; + else if (Ustrcmp(ele, "readonly") == 0) + mode = SEEN_READONLY; + else if (Ustrcmp(ele, "write") == 0) + mode = SEEN_WRITE; + else if (Ustrncmp(ele, "refresh=", 8) == 0) + { + if ((refresh = readconf_readtime(ele + 8, 0, FALSE)) < 0) + goto badparse; + } + else + goto badopt; + +if (!(dbm = dbfn_open(US"seen", O_RDWR, &dbblock, TRUE, TRUE))) + { + HDEBUG(D_acl) debug_printf_indent("database for 'seen' not available\n"); + *log_msgptr = US"database for 'seen' not available"; + return DEFER; + } + +dbd = dbfn_read_with_length(dbm, key, NULL); +now = time(NULL); +if (dbd) /* an existing record */ + { + time_t diff = now - dbd->time_stamp; /* time since the record was written */ + + if (before ? diff >= interval : diff < interval) + yield = OK; + + if (mode == SEEN_READONLY) + { HDEBUG(D_acl) debug_printf_indent("seen db not written (readonly)\n"); } + else if (mode == SEEN_WRITE || !before) + { + dbd->time_stamp = now; + dbfn_write(dbm, key, dbd, sizeof(*dbd)); + HDEBUG(D_acl) debug_printf_indent("seen db written (update)\n"); + } + else if (diff >= refresh) + { + dbd->time_stamp = now - interval; + dbfn_write(dbm, key, dbd, sizeof(*dbd)); + HDEBUG(D_acl) debug_printf_indent("seen db written (refresh)\n"); + } + } +else + { /* No record found, yield always FAIL */ + if (mode != SEEN_READONLY) + { + dbdata_seen d = {.time_stamp = now}; + dbfn_write(dbm, key, &d, sizeof(*dbd)); + HDEBUG(D_acl) debug_printf_indent("seen db written (create)\n"); + } + else + HDEBUG(D_acl) debug_printf_indent("seen db not written (readonly)\n"); + } + +dbfn_close(dbm); +return yield; + + +badparse: + *log_msgptr = string_sprintf("failed to parse '%s'", arg); + return ERROR; +badopt: + *log_msgptr = string_sprintf("unrecognised option '%s' in '%s'", ele, arg); + return ERROR; +} + + + /************************************************* * The udpsend ACL modifier * *************************************************/ @@ -2869,7 +3031,7 @@ if (*portend != '\0') } /* Make a single-item host list. */ -h = store_get(sizeof(host_item), FALSE); +h = store_get(sizeof(host_item), GET_UNTAINTED); memset(h, 0, sizeof(host_item)); h->name = hostname; h->port = portnum; @@ -3321,7 +3483,7 @@ for (; cb; cb = cb->next) { uschar * debug_tag = NULL; uschar * debug_opts = NULL; - BOOL kill = FALSE; + BOOL kill = FALSE, stop = FALSE; while (*p == '/') { @@ -3338,18 +3500,39 @@ for (; cb; cb = cb->next) } else if (Ustrncmp(pp, "kill", 4) == 0) { - for (pp += 4; *pp && *pp != '/';) pp++; + pp += 4; kill = TRUE; } - else - while (*pp && *pp != '/') pp++; + else if (Ustrncmp(pp, "stop", 4) == 0) + { + pp += 4; + stop = TRUE; + } + else if (Ustrncmp(pp, "pretrigger=", 11) == 0) + debug_pretrigger_setup(pp+11); + else if (Ustrncmp(pp, "trigger=", 8) == 0) + { + if (Ustrncmp(pp += 8, "now", 3) == 0) + { + pp += 3; + debug_trigger_fire(); + } + else if (Ustrncmp(pp, "paniclog", 8) == 0) + { + pp += 8; + dtrigger_selector |= BIT(DTi_panictrigger); + } + } + while (*pp && *pp != '/') pp++; p = pp; } - if (kill) - debug_logging_stop(); - else - debug_logging_activate(debug_tag, debug_opts); + if (kill) + debug_logging_stop(TRUE); + else if (stop) + debug_logging_stop(FALSE); + else if (debug_tag || debug_opts) + debug_logging_activate(debug_tag, debug_opts); break; } @@ -3704,22 +3887,20 @@ for (; cb; cb = cb->next) #endif case ACLC_QUEUE: + if (is_tainted(arg)) { - uschar *m; - if ((m = is_tainted2(arg, 0, "Tainted name '%s' for queue not permitted", arg))) - { - *log_msgptr = m; - return ERROR; - } - if (Ustrchr(arg, '/')) - { - *log_msgptr = string_sprintf( - "Directory separator not permitted in queue name: '%s'", arg); - return ERROR; - } - queue_name = string_copy_perm(arg, FALSE); - break; + *log_msgptr = string_sprintf("Tainted name '%s' for queue not permitted", + arg); + return ERROR; + } + if (Ustrchr(arg, '/')) + { + *log_msgptr = string_sprintf( + "Directory separator not permitted in queue name: '%s'", arg); + return ERROR; } + queue_name = string_copy_perm(arg, FALSE); + break; case ACLC_RATELIMIT: rc = acl_ratelimit(arg, where, log_msgptr); @@ -3740,6 +3921,10 @@ for (; cb; cb = cb->next) setup_remove_header(arg); break; + case ACLC_SEEN: + rc = acl_seen(arg, where, log_msgptr); + break; + case ACLC_SENDER_DOMAINS: { uschar *sdomain; @@ -4018,6 +4203,18 @@ for(;;) +/************************************************/ +/* For error messages, a string describing the config location +associated with current processing. NULL if not in an ACL. */ + +uschar * +acl_current_verb(void) +{ +if (acl_current) return string_sprintf(" (ACL %s, %s %d)", + verbs[acl_current->verb], acl_current->srcfile, acl_current->srcline); +return NULL; +} + /************************************************* * Check access using an ACL * *************************************************/ @@ -4092,10 +4289,10 @@ while (isspace(*ss)) ss++; acl_text = ss; -if ( !f.running_in_test_harness - && is_tainted2(acl_text, LOG_MAIN|LOG_PANIC, - "Tainted ACL text \"%s\"", acl_text)) +if (is_tainted(acl_text) && !f.running_in_test_harness) { + log_write(0, LOG_MAIN|LOG_PANIC, + "attempt to use tainted ACL text \"%s\"", acl_text); /* Avoid leaking info to an attacker */ *log_msgptr = US"internal configuration error"; return ERROR; @@ -4124,12 +4321,6 @@ if (Ustrchr(ss, ' ') == NULL) else if (*ss == '/') { struct stat statbuf; - if (is_tainted2(ss, LOG_MAIN|LOG_PANIC, "Tainted ACL file name '%s'", ss)) - { - /* Avoid leaking info to an attacker */ - *log_msgptr = US"internal configuration error"; - return ERROR; - } if ((fd = Uopen(ss, O_RDONLY, 0)) < 0) { *log_msgptr = string_sprintf("failed to open ACL file \"%s\": %s", ss, @@ -4144,7 +4335,7 @@ if (Ustrchr(ss, ' ') == NULL) } /* If the string being used as a filename is tainted, so is the file content */ - acl_text = store_get(statbuf.st_size + 1, is_tainted(ss)); + acl_text = store_get(statbuf.st_size + 1, ss); acl_text_end = acl_text + statbuf.st_size + 1; if (read(fd, acl_text, statbuf.st_size) != statbuf.st_size) @@ -4174,7 +4365,7 @@ if (!acl) if (!acl && *log_msgptr) return ERROR; if (fd >= 0) { - tree_node *t = store_get_perm(sizeof(tree_node) + Ustrlen(ss), is_tainted(ss)); + tree_node * t = store_get_perm(sizeof(tree_node) + Ustrlen(ss), ss); Ustrcpy(t->name, ss); t->data.ptr = acl; (void)tree_insertnode(&acl_anchor, t); @@ -4183,7 +4374,7 @@ if (!acl) /* Now we have an ACL to use. It's possible it may be NULL. */ -while (acl) +while ((acl_current = acl)) { int cond; int basic_errno = 0; @@ -4330,8 +4521,8 @@ while (acl) else if (cond == DEFER && LOGGING(acl_warn_skipped)) log_write(0, LOG_MAIN, "%s Warning: ACL \"warn\" statement skipped: " "condition test deferred%s%s", host_and_ident(TRUE), - (*log_msgptr == NULL)? US"" : US": ", - (*log_msgptr == NULL)? US"" : *log_msgptr); + *log_msgptr ? US": " : US"", + *log_msgptr ? *log_msgptr : US""); *log_msgptr = *user_msgptr = NULL; /* In case implicit DENY follows */ break; @@ -4656,7 +4847,7 @@ acl_var_create(uschar * name) tree_node * node, ** root = name[0] == 'c' ? &acl_var_c : &acl_var_m; if (!(node = tree_search(*root, name))) { - node = store_get(sizeof(tree_node) + Ustrlen(name), is_tainted(name)); + node = store_get(sizeof(tree_node) + Ustrlen(name), name); Ustrcpy(node->name, name); (void)tree_insertnode(root, node); } @@ -4687,13 +4878,20 @@ Returns: nothing */ void -acl_var_write(uschar *name, uschar *value, void *ctx) +acl_var_write(uschar * name, uschar * value, void * ctx) { -FILE *f = (FILE *)ctx; -if (is_tainted(value)) putc('-', f); -fprintf(f, "-acl%c %s %d\n%s\n", name[0], name+1, Ustrlen(value), value); +FILE * f = (FILE *)ctx; +putc('-', f); +if (is_tainted(value)) + { + int q = quoter_for_address(value); + putc('-', f); + if (is_real_quoter(q)) fprintf(f, "(%s)", lookup_list[q]->name); + } +fprintf(f, "acl%c %s %d\n%s\n", name[0], name+1, Ustrlen(value), value); } +#endif /* !MACRO_PREDEF */ /* vi: aw ai sw=2 */ /* End of acl.c */