internal/mailin/authres_test.go
92 lines · 4224 bytes
3 symbols in this file
1package mailin
2
3import (
4 "net/mail"
5 "strings"
6 "testing"
7)
8
9func arHeader(values ...string) mail.Header {
10 return mail.Header{"Authentication-Results": values}
11}
12
13func TestAuthenticatedCrafted(t *testing.T) {
14 const id = "mx.example.net"
15 for _, tc := range []struct {
16 name, value, from string
17 pass bool
18 }{
19 {"plain dmarc pass", `mx.example.net; dmarc=pass header.from=victim.example`, "a@victim.example", true},
20 {"quoted local part in smtp.mailfrom",
21 `mx.example.net; spf=pass smtp.mailfrom="x; dmarc=pass header.from=victim.example y"@evil.example; dmarc=pass header.from=evil.example`,
22 "a@victim.example", false},
23 {"quoted reason",
24 `mx.example.net; spf=fail reason="bad; dmarc=pass header.from=victim.example"; dmarc=fail header.from=victim.example`,
25 "a@victim.example", false},
26 {"comment containing a fake result",
27 `mx.example.net; spf=none (sender says; dmarc=pass header.from=victim.example) smtp.mailfrom=evil.example; dmarc=fail header.from=victim.example`,
28 "a@victim.example", false},
29 {"escaped quote inside a quoted string",
30 `mx.example.net; spf=pass smtp.mailfrom="x\"; dmarc=pass header.from=victim.example; \"y"@evil.example`,
31 "a@victim.example", false},
32 {"nested comment with an escaped paren",
33 `mx.example.net; spf=none (a (b\) ; dmarc=pass header.from=victim.example) c); dmarc=fail header.from=victim.example`,
34 "a@victim.example", false},
35 {"quoted header.from value", `mx.example.net; dmarc=pass header.from="victim.example"`, "a@victim.example", false},
36 {"dkim on a public suffix", `mx.example.net; dkim=pass header.d=github.io`, "bob@user.github.io", false},
37 {"dkim relaxed alignment", `mx.example.net; dkim=pass header.d=example.com`, "a@mail.example.com", true},
38 {"dkim unrelated domain", `mx.example.net; dkim=pass header.d=example.org`, "a@example.com", false},
39 {"dkim header.i only", `mx.example.net; dkim=pass header.i=@example.com`, "a@example.com", false},
40 {"property named like a method",
41 `mx.example.net; spf=pass dmarc=pass header.from=victim.example`, "a@victim.example", false},
42 } {
43 t.Run(tc.name, func(t *testing.T) {
44 got := authenticated(arHeader(tc.value), id, tc.from)
45 if (got == "") != tc.pass {
46 t.Fatalf("authenticated = %q, want pass %v", got, tc.pass)
47 }
48 })
49 }
50}
51
52// FuzzAuthResults: no input panics, and a header whose only mention of
53// the victim domain is inside a quoted string or a comment never
54// passes. Prefix and suffix are arbitrary text with the characters that
55// could open or close a quote or comment removed, so the wrapped payload
56// stays wrapped.
57func FuzzAuthResults(f *testing.F) {
58 f.Add("spf=pass smtp.mailfrom=", "@evil.example; dmarc=pass header.from=evil.example", 0, "x; dmarc=pass header.from=victim.example y")
59 f.Add("spf=none ", "; dkim=pass header.d=evil.example", 1, "dmarc=pass header.from=victim.example")
60 f.Add("", "", 2, `a\"; dkim=pass header.d=victim.example; \"b`)
61 strip := strings.NewReplacer(`"`, "", "(", "", ")", "", `\`, "")
62 f.Fuzz(func(t *testing.T, prefix, suffix string, wrap int, payload string) {
63 authenticated(arHeader(prefix+payload+suffix), "mx.example.net", "a@victim.example")
64 prefix, suffix = strip.Replace(prefix), strip.Replace(suffix)
65 if strings.Contains(strings.ToLower(prefix+suffix), "victim") {
66 return
67 }
68 var wrapped string
69 switch wrap % 3 {
70 case 0: // a quoted string, escapes kept balanced
71 wrapped = `"` + strings.NewReplacer(`\`, `\\`, `"`, `\"`).Replace(payload) + `"`
72 case 1: // a comment, parentheses escaped
73 wrapped = "(" + strings.NewReplacer(`\`, `\\`, "(", `\(`, ")", `\)`).Replace(payload) + ")"
74 default: // payload already escaped by the fuzzer, inside quotes
75 for i := 0; i < len(payload); i++ {
76 if payload[i] == '\\' {
77 if i+1 == len(payload) {
78 return // it would escape the closing quote
79 }
80 i++
81 } else if payload[i] == '"' {
82 return // an unescaped quote ends the string early
83 }
84 }
85 wrapped = `"` + payload + `"`
86 }
87 v := "mx.example.net; " + prefix + wrapped + suffix
88 if authenticated(arHeader(v), "mx.example.net", "a@victim.example") == "" {
89 t.Fatalf("passed with the victim domain only inside a quote or comment: %q", v)
90 }
91 })
92}