SPF: harden against crafted DNS responses
[exim.git] / src / src / spf.c
1 /*************************************************
2 *     Exim - an Internet mail transport agent    *
3 *************************************************/
4
5 /* SPF support.
6    Copyright (c) The Exim Maintainers 2015 - 2022
7    Copyright (c) Tom Kistner <tom@duncanthrax.net> 2004 - 2014
8    License: GPL
9 */
10
11 /* Code for calling spf checks via libspf-alt. Called from acl.c. */
12
13 #include "exim.h"
14 #ifdef SUPPORT_SPF
15
16 /* must be kept in numeric order */
17 static spf_result_id spf_result_id_list[] = {
18   /* name               value */
19   { US"invalid",        0},
20   { US"neutral",        1 },
21   { US"pass",           2 },
22   { US"fail",           3 },
23   { US"softfail",       4 },
24   { US"none",           5 },
25   { US"temperror",      6 }, /* RFC 4408 defined */
26   { US"permerror",      7 }  /* RFC 4408 defined */
27 };
28
29 SPF_server_t    *spf_server = NULL;
30 SPF_request_t   *spf_request = NULL;
31 SPF_response_t  *spf_response = NULL;
32 SPF_response_t  *spf_response_2mx = NULL;
33
34 SPF_dns_rr_t  * spf_nxdomain = NULL;
35
36
37 gstring *
38 spf_lib_version_report(gstring * g)
39 {
40 int maj, min, patch;
41
42 SPF_get_lib_version(&maj, &min, &patch);
43 g = string_fmt_append(g, "Library version: spf2: Compile: %d.%d.%d\n",
44         SPF_LIB_VERSION_MAJOR, SPF_LIB_VERSION_MINOR, SPF_LIB_VERSION_PATCH);
45 g = string_fmt_append(g,    "                       Runtime: %d.%d.%d\n",
46          maj, min, patch);
47 return g;
48 }
49
50
51
52 static SPF_dns_rr_t *
53 SPF_dns_exim_lookup(SPF_dns_server_t *spf_dns_server,
54   const char *domain, ns_type rr_type, int should_cache)
55 {
56 dns_answer * dnsa = store_get_dns_answer();
57 dns_scan dnss;
58 SPF_dns_rr_t * spfrr;
59 unsigned found = 0;
60
61 SPF_dns_rr_t srr = {
62   .domain = CS domain,                  /* query information */
63   .domain_buf_len = 0,
64   .rr_type = rr_type,
65
66   .rr_buf_len = 0,                      /* answer information */
67   .rr_buf_num = 0, /* no free of s */
68   .utc_ttl = 0,
69
70   .hook = NULL,                         /* misc information */
71   .source = spf_dns_server
72 };
73 int dns_rc;
74
75 DEBUG(D_receive) debug_printf("SPF_dns_exim_lookup '%s'\n", domain);
76
77 /* Shortcircuit SPF RR lookups by returning NO_DATA.  They were obsoleted by
78 RFC 6686/7208 years ago. see bug #1294 */
79
80 if (rr_type == T_SPF)
81   {
82   HDEBUG(D_host_lookup) debug_printf("faking NO_DATA for SPF RR(99) lookup\n");
83   srr.herrno = NO_DATA;
84   SPF_dns_rr_dup(&spfrr, &srr);
85   store_free_dns_answer(dnsa);
86   return spfrr;
87   }
88
89 switch (dns_rc = dns_lookup(dnsa, US domain, rr_type, NULL))
90   {
91   case DNS_SUCCEED:     srr.herrno = NETDB_SUCCESS;     break;
92   case DNS_AGAIN:       srr.herrno = TRY_AGAIN;         break;
93   case DNS_NOMATCH:     srr.herrno = HOST_NOT_FOUND;    break;
94   case DNS_NODATA:      srr.herrno = NO_DATA;           break;
95   case DNS_FAIL:
96   default:              srr.herrno = NO_RECOVERY;       break;
97   }
98
99 for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
100      rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
101   if (rr->type == rr_type) found++;
102
103 if (found == 0)
104   {
105   SPF_dns_rr_dup(&spfrr, &srr);
106   store_free_dns_answer(dnsa);
107   return spfrr;
108   }
109
110 srr.rr = store_malloc(sizeof(SPF_dns_rr_data_t) * found);
111
112 found = 0;
113 for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
114    rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
115   if (rr->type == rr_type)
116     {
117     const uschar * s = rr->data;
118
119     srr.ttl = rr->ttl;
120     switch(rr_type)
121       {
122       case T_MX:
123         if (rr->size < 2) continue;
124         s += 2; /* skip the MX precedence field */
125       case T_PTR:
126         {
127         uschar * buf = store_malloc(256);
128         (void)dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, s,
129           (DN_EXPAND_ARG4_TYPE)buf, 256);
130         s = buf;
131         break;
132         }
133
134       case T_TXT:
135         {
136         gstring * g = NULL;
137         uschar chunk_len;
138
139         if (rr->size < 1+6) continue;           /* min for version str */
140         if (strncmpic(rr->data+1, US SPF_VER_STR, 6) != 0)
141           {
142           HDEBUG(D_host_lookup) debug_printf("not an spf record: %.*s\n",
143             (int) s[0], s+1);
144           continue;
145           }
146
147         /* require 1 byte for the chunk_len */
148         for (int off = 0; off < rr->size - 1; off += chunk_len)
149           {
150           if (  !(chunk_len = s[off++])
151              || rr->size < off + chunk_len      /* ignore bogus size chunks */
152              ) break;
153           g = string_catn(g, s+off, chunk_len);
154           }
155         if (!g)
156           continue;
157         gstring_release_unused(g);
158         s = string_copy_malloc(string_from_gstring(g));
159         DEBUG(D_receive) debug_printf("SPF_dns_exim_lookup '%s'\n", s);
160         break;
161         }
162
163       case T_A:
164       case T_AAAA:
165       default:
166         {
167         uschar * buf = store_malloc(dnsa->answerlen + 1);
168         s = memcpy(buf, s, dnsa->answerlen + 1);
169         break;
170         }
171       }
172     srr.rr[found++] = (void *) s;
173     }
174
175 /* Did we filter out all TXT RRs? Return NO_DATA instead of SUCCESS with
176 empty ANSWER section. */
177
178 if (!(srr.num_rr = found))
179   srr.herrno = NO_DATA;
180
181 /* spfrr->rr must have been malloc()d for this */
182 SPF_dns_rr_dup(&spfrr, &srr);
183 store_free_dns_answer(dnsa);
184 return spfrr;
185 }
186
187
188
189 SPF_dns_server_t *
190 SPF_dns_exim_new(int debug)
191 {
192 SPF_dns_server_t * spf_dns_server = store_malloc(sizeof(SPF_dns_server_t));
193
194 DEBUG(D_receive) debug_printf("SPF_dns_exim_new\n");
195
196 memset(spf_dns_server, 0, sizeof(SPF_dns_server_t));
197 spf_dns_server->destroy      = NULL;
198 spf_dns_server->lookup       = SPF_dns_exim_lookup;
199 spf_dns_server->get_spf      = NULL;
200 spf_dns_server->get_exp      = NULL;
201 spf_dns_server->add_cache    = NULL;
202 spf_dns_server->layer_below  = NULL;
203 spf_dns_server->name         = "exim";
204 spf_dns_server->debug        = debug;
205
206 /* XXX This might have to return NO_DATA sometimes. */
207
208 spf_nxdomain = SPF_dns_rr_new_init(spf_dns_server,
209   "", ns_t_any, 24 * 60 * 60, HOST_NOT_FOUND);
210 if (!spf_nxdomain)
211   {
212   free(spf_dns_server);
213   return NULL;
214   }
215
216 return spf_dns_server;
217 }
218
219
220
221
222 /* Construct the SPF library stack.
223    Return: Boolean success.
224 */
225
226 BOOL
227 spf_init(void)
228 {
229 SPF_dns_server_t * dc;
230 int debug = 0;
231 const uschar *s;
232
233 DEBUG(D_receive) debug = 1;
234
235 /* We insert our own DNS access layer rather than letting the spf library
236 do it, so that our dns access path is used for debug tracing and for the
237 testsuite. */
238
239 if (!(dc = SPF_dns_exim_new(debug)))
240   {
241   DEBUG(D_receive) debug_printf("spf: SPF_dns_exim_new() failed\n");
242   return FALSE;
243   }
244 if (!(dc = SPF_dns_cache_new(dc, NULL, debug, 8)))
245   {
246   DEBUG(D_receive) debug_printf("spf: SPF_dns_cache_new() failed\n");
247   return FALSE;
248   }
249 if (!(spf_server = SPF_server_new_dns(dc, debug)))
250   {
251   DEBUG(D_receive) debug_printf("spf: SPF_server_new() failed.\n");
252   return FALSE;
253   }
254   /* Override the outdated explanation URL.
255   See https://www.mail-archive.com/mailop@mailop.org/msg08019.html
256   Used to work as "Please%_see%_http://www.open-spf.org/Why?id=%{S}&ip=%{C}&receiver=%{R}",
257   but is broken now (May 18th, 2020) */
258 if (!(s = expand_string(spf_smtp_comment_template)))
259   log_write(0, LOG_MAIN|LOG_PANIC_DIE, "expansion of spf_smtp_comment_template failed");
260
261 SPF_server_set_explanation(spf_server, CCS s, &spf_response);
262 if (SPF_response_errcode(spf_response) != SPF_E_SUCCESS)
263   log_write(0, LOG_MAIN|LOG_PANIC_DIE, "%s", SPF_strerror(SPF_response_errcode(spf_response)));
264
265 return TRUE;
266 }
267
268
269 /* Set up a context that can be re-used for several
270    messages on the same SMTP connection (that come from the
271    same host with the same HELO string).
272
273 Return: Boolean success
274 */
275
276 BOOL
277 spf_conn_init(uschar * spf_helo_domain, uschar * spf_remote_addr)
278 {
279 DEBUG(D_receive)
280   debug_printf("spf_conn_init: %s %s\n", spf_helo_domain, spf_remote_addr);
281
282 if (!spf_server && !spf_init()) return FALSE;
283
284 if (SPF_server_set_rec_dom(spf_server, CS primary_hostname))
285   {
286   DEBUG(D_receive) debug_printf("spf: SPF_server_set_rec_dom(\"%s\") failed.\n",
287     primary_hostname);
288   spf_server = NULL;
289   return FALSE;
290   }
291
292 spf_request = SPF_request_new(spf_server);
293
294 if (  SPF_request_set_ipv4_str(spf_request, CS spf_remote_addr)
295    && SPF_request_set_ipv6_str(spf_request, CS spf_remote_addr)
296    )
297   {
298   DEBUG(D_receive)
299     debug_printf("spf: SPF_request_set_ipv4_str() and "
300       "SPF_request_set_ipv6_str() failed [%s]\n", spf_remote_addr);
301   spf_server = NULL;
302   spf_request = NULL;
303   return FALSE;
304   }
305
306 if (SPF_request_set_helo_dom(spf_request, CS spf_helo_domain))
307   {
308   DEBUG(D_receive) debug_printf("spf: SPF_set_helo_dom(\"%s\") failed.\n",
309     spf_helo_domain);
310   spf_server = NULL;
311   spf_request = NULL;
312   return FALSE;
313   }
314
315 return TRUE;
316 }
317
318
319 void
320 spf_response_debug(SPF_response_t * spf_response)
321 {
322 if (SPF_response_messages(spf_response) == 0)
323   debug_printf(" (no errors)\n");
324 else for (int i = 0; i < SPF_response_messages(spf_response); i++)
325   {
326   SPF_error_t * err = SPF_response_message(spf_response, i);
327   debug_printf( "%s_msg = (%d) %s\n",
328                   (SPF_error_errorp(err) ? "warn" : "err"),
329                   SPF_error_code(err),
330                   SPF_error_message(err));
331   }
332 }
333
334
335 /* spf_process adds the envelope sender address to the existing
336    context (if any), retrieves the result, sets up expansion
337    strings and evaluates the condition outcome.
338
339 Return: OK/FAIL  */
340
341 int
342 spf_process(const uschar **listptr, uschar *spf_envelope_sender, int action)
343 {
344 int sep = 0;
345 const uschar *list = *listptr;
346 uschar *spf_result_id;
347 int rc = SPF_RESULT_PERMERROR;
348
349 DEBUG(D_receive) debug_printf("spf_process\n");
350
351 if (!(spf_server && spf_request))
352   /* no global context, assume temp error and skip to evaluation */
353   rc = SPF_RESULT_PERMERROR;
354
355 else if (SPF_request_set_env_from(spf_request, CS spf_envelope_sender))
356   /* Invalid sender address. This should be a real rare occurrence */
357   rc = SPF_RESULT_PERMERROR;
358
359 else
360   {
361   /* get SPF result */
362   if (action == SPF_PROCESS_FALLBACK)
363     {
364     SPF_request_query_fallback(spf_request, &spf_response, CS spf_guess);
365     spf_result_guessed = TRUE;
366     }
367   else
368     SPF_request_query_mailfrom(spf_request, &spf_response);
369
370   /* set up expansion items */
371   spf_header_comment     = US SPF_response_get_header_comment(spf_response);
372   spf_received           = US SPF_response_get_received_spf(spf_response);
373   spf_result             = US SPF_strresult(SPF_response_result(spf_response));
374   spf_smtp_comment       = US SPF_response_get_smtp_comment(spf_response);
375
376   rc = SPF_response_result(spf_response);
377
378   DEBUG(D_acl) spf_response_debug(spf_response);
379   }
380
381 /* We got a result. Now see if we should return OK or FAIL for it */
382 DEBUG(D_acl) debug_printf("SPF result is %s (%d)\n", SPF_strresult(rc), rc);
383
384 if (action == SPF_PROCESS_GUESS && (!strcmp (SPF_strresult(rc), "none")))
385   return spf_process(listptr, spf_envelope_sender, SPF_PROCESS_FALLBACK);
386
387 while ((spf_result_id = string_nextinlist(&list, &sep, NULL, 0)))
388   {
389   BOOL negate, result;
390
391   if ((negate = spf_result_id[0] == '!'))
392     spf_result_id++;
393
394   result = Ustrcmp(spf_result_id, spf_result_id_list[rc].name) == 0;
395   if (negate != result) return OK;
396   }
397
398 /* no match */
399 return FAIL;
400 }
401
402
403
404 gstring *
405 authres_spf(gstring * g)
406 {
407 uschar * s;
408 if (!spf_result) return g;
409
410 g = string_append(g, 2, US";\n\tspf=", spf_result);
411 if (spf_result_guessed)
412   g = string_cat(g, US" (best guess record for domain)");
413
414 s = expand_string(US"$sender_address_domain");
415 if (s && *s)
416   return string_append(g, 2, US" smtp.mailfrom=", s);
417
418 s = sender_helo_name;
419 return s && *s
420   ? string_append(g, 2, US" smtp.helo=", s)
421   : string_cat(g, US" smtp.mailfrom=<>");
422 }
423
424
425 #endif