internal/mailin/authres_test.go

main
gitbay/internal/mailin/authres_test.go history · blame · raw

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}