git » spf » commit 774fb1b

a, mx: Reject malformed fields without domain

author Alberto Bertogli
2026-08-21 21:50:01 UTC
committer Alberto Bertogli
2026-08-22 09:01:21 UTC
parent a2c34380020b65b39520a2decd74aed0713ccd26

a, mx: Reject malformed fields without domain

Fields like "a:" or "mx:/24" are malformed, but currently we accept them
with the current domain. This is fairly harmless in general but can
cause interoperability problems, and checkers to return wrongly positive
results.

This patch fixes the bug by explicitly rejecting those kinds of
malformed fields.

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