| author | Alberto Bertogli
<albertito@blitiri.com.ar> 2026-08-21 21:50:01 UTC |
| committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2026-08-22 09:01:21 UTC |
| parent | a2c34380020b65b39520a2decd74aed0713ccd26 |
| spf.go | +28 | -23 |
| spf_test.go | +14 | -0 |
| testdata/rfc4408-tests.yml | +0 | -2 |
| testdata/rfc7208-tests.yml | +0 | -2 |
diff --git a/spf.go b/spf.go index 04d7034..9c302f8 100644 --- a/spf.go +++ b/spf.go @@ -852,32 +852,37 @@ var mxRegexp = regexp.MustCompile(`^[mM][xX](:([^/]+))?(/(\w+))?(//(\w+))?$`) func domainAndMask(re *regexp.Regexp, field, domain string) (string, dualMasks, error) { masks := dualMasks{} groups := re.FindStringSubmatch(field) - if groups != nil { - if groups[2] != "" { - domain = groups[2] - } - if groups[4] != "" { - i, err := strconv.Atoi(groups[4]) - mask4 := net.CIDRMask(i, 32) - if err != nil || mask4 == nil { - return "", masks, ErrInvalidMask - } - masks.v4 = mask4 - } - if groups[6] != "" { - i, err := strconv.Atoi(groups[6]) - mask6 := net.CIDRMask(i, 128) - if err != nil || mask6 == nil { - return "", masks, ErrInvalidMask - } - masks.v6 = mask6 + if groups == nil { + // In the regexp we enforce that if ":" is used, there must be a + // domain after it, and if "/" is used, there must be at least one + // character after it. So if we found no groups, it must be malformed. + // For example, "a:", "mx:/24", "a//", "a:x/". + // We use the presence of "/" to decide which error to return. + // https://tools.ietf.org/html/rfc7208#section-5.3 + if strings.Contains(field, "/") { + return "", masks, ErrInvalidMask } + return "", masks, ErrInvalidDomain } - // Test to catch malformed entries: if there's a /, there must be at least - // one mask. - if strings.Contains(field, "/") && masks.v4 == nil && masks.v6 == nil { - return "", masks, ErrInvalidMask + if groups[2] != "" { + domain = groups[2] + } + if groups[4] != "" { + i, err := strconv.Atoi(groups[4]) + mask4 := net.CIDRMask(i, 32) + if err != nil || mask4 == nil { + return "", masks, ErrInvalidMask + } + masks.v4 = mask4 + } + if groups[6] != "" { + i, err := strconv.Atoi(groups[6]) + mask6 := net.CIDRMask(i, 128) + if err != nil || mask6 == nil { + return "", masks, ErrInvalidMask + } + masks.v6 = mask6 } return domain, masks, nil diff --git a/spf_test.go b/spf_test.go index fb11565..552110c 100644 --- a/spf_test.go +++ b/spf_test.go @@ -79,6 +79,20 @@ func TestBasic(t *testing.T) { {"v=spf1 exists:d1111 -all", Pass, ErrMatchedExists}, {"v=spf1 redirect=", PermError, ErrInvalidDomain}, + // An empty domain-spec is not allowed; it must not be silently + // treated as the current domain. + // https://tools.ietf.org/html/rfc7208#section-5.3 + {"v=spf1 a: -all", PermError, ErrInvalidDomain}, + {"v=spf1 mx: -all", PermError, ErrInvalidDomain}, + {"v=spf1 a:/24 -all", PermError, ErrInvalidMask}, + {"v=spf1 mx:/24 -all", PermError, ErrInvalidMask}, + {"v=spf1 a:// -all", PermError, ErrInvalidMask}, + + // While the ones with an actual domain still work. + {"v=spf1 a:d1111 -all", Pass, ErrMatchedA}, + {"v=spf1 a -all", Fail, ErrMatchedAll}, + {"v=spf1 mx -all", Fail, ErrMatchedAll}, + // Unrecognized modifiers are ignored, so evaluation continues. // https://tools.ietf.org/html/rfc7208#section-6 {"v=spf1 ra=postmaster -all", Fail, ErrMatchedAll}, diff --git a/testdata/rfc4408-tests.yml b/testdata/rfc4408-tests.yml index 898d43c..f16cc83 100644 --- a/testdata/rfc4408-tests.yml +++ b/testdata/rfc4408-tests.yml @@ -919,7 +919,6 @@ tests: host: 1.2.3.4 mailfrom: foo@e13.example.com result: permerror - skip: Not worth the complexity of erroring on this. zonedata: mail.example.com: - A: 1.2.3.4 @@ -1284,7 +1283,6 @@ tests: host: 1.2.3.4 mailfrom: foo@e13.example.com result: permerror - skip: Not worth the complexity of erroring on this. zonedata: mail.example.com: - A: 1.2.3.4 diff --git a/testdata/rfc7208-tests.yml b/testdata/rfc7208-tests.yml index d730242..3dbd041 100644 --- a/testdata/rfc7208-tests.yml +++ b/testdata/rfc7208-tests.yml @@ -1012,7 +1012,6 @@ tests: host: 1.2.3.4 mailfrom: foo@e13.example.com result: permerror - skip: Not worth the complexity of erroring on this. zonedata: mail.example.com: - A: 1.2.3.4 @@ -1379,7 +1378,6 @@ tests: host: 1.2.3.4 mailfrom: foo@e13.example.com result: permerror - skip: Not worth the complexity of erroring on this. zonedata: mail.example.com: - A: 1.2.3.4