diff --git a/spf.go b/spf.go index be9d585..cb38ffe 100644 --- a/spf.go +++ b/spf.go @@ -95,6 +95,7 @@ var ( ErrNoResult = errors.New("no DNS record found") ErrLookupLimitReached = errors.New("lookup limit reached") ErrVoidLookupLimitReached = errors.New("void lookup limit reached") + ErrRecordTooLong = errors.New("DNS record is too long") ErrTooManyMXRecords = errors.New("too many MX records") ErrMultipleRecords = errors.New("multiple matching DNS records") @@ -118,6 +119,11 @@ const ( // with a configurable default of 2. // https://tools.ietf.org/html/rfc7208#section-4.6.4 defaultMaxVoidLookups = 2 + + // Default value for maximum octet size of the SPF response. The + // RFC suggests the limit should be 450. + // https://tools.ietf.org/html/rfc7208#section-3.4 + defaultMaxRecordLength = 450 ) // TraceFunc is the type of tracing functions. @@ -147,14 +153,15 @@ type Option func(*resolution) // Deprecated: use CheckHostWithSender instead. func CheckHost(ip net.IP, domain string) (Result, error) { r := &resolution{ - ip: ip, - maxcount: defaultMaxLookups, - maxvoidcount: defaultMaxVoidLookups, - helo: domain, - sender: "@" + domain, - ctx: context.TODO(), - resolver: defaultResolver, - trace: defaultTrace, + ip: ip, + maxcount: defaultMaxLookups, + maxvoidcount: defaultMaxVoidLookups, + maxresponsesize: defaultMaxRecordLength, + helo: domain, + sender: "@" + domain, + ctx: context.TODO(), + resolver: defaultResolver, + trace: defaultTrace, } return r.Check(domain) } @@ -178,14 +185,15 @@ func CheckHostWithSender(ip net.IP, helo, sender string, opts ...Option) (Result } r := &resolution{ - ip: ip, - maxcount: defaultMaxLookups, - maxvoidcount: defaultMaxVoidLookups, - helo: helo, - sender: sender, - ctx: context.TODO(), - resolver: defaultResolver, - trace: defaultTrace, + ip: ip, + maxcount: defaultMaxLookups, + maxvoidcount: defaultMaxVoidLookups, + maxresponsesize: defaultMaxRecordLength, + helo: helo, + sender: sender, + ctx: context.TODO(), + resolver: defaultResolver, + trace: defaultTrace, } for _, opt := range opts { @@ -219,6 +227,18 @@ func OverrideVoidLookupLimit(limit uint) Option { } } +// OverrideOctetLimit overrides the maximum octet size allowed +// during SPF evaluation. +// Note that as per RFC, the default value of 450 SHOULD +// be used. Please use with care. +// +// This is EXPERIMENTAL for now, and the API is subject to change. +func OverrideOctetLimit(limit int) Option { + return func(r *resolution) { + r.maxresponsesize = limit + } +} + // WithContext is an option to set the context for this operation, which will // be passed along to the resolver functions and other external calls if // needed. @@ -246,8 +266,7 @@ var defaultResolver DNSResolver = net.DefaultResolver // // The default is to use net.DefaultResolver, which should be appropriate for // most users. -// -// This is EXPERIMENTAL for now, and the API is subject to change. +//https://tools.ietf.org/html/rfc7208#section-3.4 func WithResolver(resolver DNSResolver) Option { return func(r *resolution) { r.resolver = resolver @@ -278,11 +297,12 @@ func split(addr string) (string, string) { } type resolution struct { - ip net.IP - count uint - maxcount uint - voidcount uint - maxvoidcount uint + ip net.IP + count uint + maxcount uint + voidcount uint + maxvoidcount uint + maxresponsesize int helo string sender string @@ -329,6 +349,13 @@ func (r *resolution) Check(domain string) (Result, error) { return None, ErrNoResult } + if len(txt) > r.maxresponsesize { + // SPF response greater than 450 octets + // https://tools.ietf.org/html/rfc7208#section-3.4 + r.trace("dns perm error: %s response size %d", ErrRecordTooLong, len(txt)) + return PermError, ErrRecordTooLong + } + fields := strings.Split(txt, " ") // redirects must be handled after the rest; instead of having two loops, diff --git a/spf_test.go b/spf_test.go index bae1098..f9b3d94 100644 --- a/spf_test.go +++ b/spf_test.go @@ -671,3 +671,25 @@ func TestWithTraceFunc(t *testing.T) { t.Errorf("expected >0 trace function calls, got 0") } } + +func TestOctetSizeLimit(t *testing.T) { + dns := NewDefaultResolver() + dns.Txt["domain"] = []string{"v=spf1 ip4:192.168.168.0/24 ip4:192.168.169.0/24 ip4:192.168.170.0/24 ip4:192.168.171.0/24 ip4:192.168.172.0/24 ip4:192.168.173.0/24 ip4:192.168.174.0/24 ip4:192.168.175.0/24 ip4:192.168.176.0/24 ip4:192.168.177.0/24 ip4:192.168.178.0/24 ip4:192.168.179.0/24 ip4:192.168.180.0/24 ip4:192.168.181.0/24 ip4:192.168.182.0/24 ip4:192.168.183.0/24 ip4:192.168.184.0/24 ip4:192.168.185.0/24 ip4:192.168.186.0/24 ip4:192.168.187.0/24 ip4:192.168.188.0/24 ip4:192.168.189.0/24 ip4:192.168.190.0/24 ~all"} + defaultTrace = t.Logf + + res, err := CheckHost(ip1111, "domain") + if res != PermError && err != ErrRecordTooLong { + t.Errorf("expected permerror, got %v (%v)", res, err) + } +} + +func TestOverrideOctetSizeLimit(t *testing.T) { + dns := NewDefaultResolver() + dns.Txt["domain"] = []string{"v=spf1 ip4:1.1.1.1 ip4:192.168.169.0/24 ip4:192.168.170.0/24 ip4:192.168.171.0/24 ip4:192.168.172.0/24 ip4:192.168.173.0/24 ip4:192.168.174.0/24 ip4:192.168.175.0/24 ip4:192.168.176.0/24 ip4:192.168.177.0/24 ip4:192.168.178.0/24 ip4:192.168.179.0/24 ip4:192.168.180.0/24 ip4:192.168.181.0/24 ip4:192.168.182.0/24 ip4:192.168.183.0/24 ip4:192.168.184.0/24 ip4:192.168.185.0/24 ip4:192.168.186.0/24 ip4:192.168.187.0/24 ip4:192.168.188.0/24 ip4:192.168.189.0/24 ip4:192.168.190.0/24 ~all"} + defaultTrace = t.Logf + + res, err := CheckHostWithSender(ip1111, "helo", "user@domain", OverrideOctetLimit(500)) + if res != Pass { + t.Errorf("expected permerror, got %v (%v)", res, err) + } +}