git » spf » commit 1e7306a

Ignore unrecognized modifiers

author Alberto Bertogli
2026-08-21 21:01:56 UTC
committer Alberto Bertogli
2026-08-22 09:01:21 UTC
parent 2868f0bfddcd7e785526155486bb06469418a45b

Ignore unrecognized modifiers

Currently, if we see any unrecognized modifiers, we return permerror.

This is against the standard, which says they should be ignored:

> Unrecognized modifiers MUST be ignored no matter where, or how often,
> they appear in a record.  This allows implementations conforming to
> this document to gracefully handle records with modifiers that are
> defined in other specifications.

https://tools.ietf.org/html/rfc7208#section-6

So this patch explicitly identifies unrecognized modifiers, and ignores
them.

While this is rare in the wild, some places may still be using the
pre-standard "ra=", "rp=" and "rr=" modifiers.

Existing standard tests cover this scenario, but unfortunately were
incorrectly marked as skipped a long time ago. This patch also removes
the skip, incorporating the tests.

spf.go +24 -0
spf_test.go +19 -0
testdata/rfc4408-tests.yml +0 -1
testdata/rfc7208-tests.yml +0 -1

diff --git a/spf.go b/spf.go
index 403a7d2..d1154f3 100644
--- a/spf.go
+++ b/spf.go
@@ -318,6 +318,17 @@ type resolution struct {
 	trace TraceFunc
 }
 
+// A modifier is a name and a value, separated by "=":
+//
+//	modifier = redirect / explanation / unknown-modifier
+//	unknown-modifier = name "=" macro-string
+//	name = ALPHA *( ALPHA / DIGIT / "-" / "_" / "." )
+//
+// Note the "=" comes before any ":" or "/", which is what tells modifiers
+// apart from mechanisms.
+// https://tools.ietf.org/html/rfc7208#section-4.6.1
+var modifierRegexp = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9._-]*=`)
+
 var aField = regexp.MustCompile(`^(a$|a:|a/)`)
 var mxField = regexp.MustCompile(`^(mx$|mx:|mx/)`)
 var ptrField = regexp.MustCompile(`^(ptr$|ptr:)`)
@@ -385,6 +396,13 @@ func (r *resolution) Check(domain string) (Result, error) {
 			continue
 		}
 
+		// Is this a modifier? Note we check this before stripping the
+		// qualifier below, because modifiers don't take one:
+		//   directive = [ qualifier ] mechanism
+		//   modifier  = redirect / explanation / unknown-modifier
+		// https://tools.ietf.org/html/rfc7208#section-4.6.1
+		isModifier := modifierRegexp.MatchString(field)
+
 		// See if we have a qualifier, defaulting to + (pass).
 		// https://tools.ietf.org/html/rfc7208#section-4.6.2
 		result, ok := qualToResult[field[0]]
@@ -439,6 +457,12 @@ func (r *resolution) Check(domain string) (Result, error) {
 			res, err := r.redirectField(field, domain)
 			r.trace("%q: %v, %v", field, res, err)
 			return res, err
+		} else if isModifier {
+			// Unrecognized modifiers must be ignored, so that records
+			// using modifiers defined elsewhere still work.
+			// https://tools.ietf.org/html/rfc7208#section-6
+			r.trace("unknown modifier, ignoring")
+			continue
 		} else {
 			r.trace("unknown field, permerror")
 			return PermError, ErrUnknownField
diff --git a/spf_test.go b/spf_test.go
index 980424e..e527aa0 100644
--- a/spf_test.go
+++ b/spf_test.go
@@ -78,6 +78,25 @@ func TestBasic(t *testing.T) {
 		{"v=spf1 blah", PermError, ErrUnknownField},
 		{"v=spf1 exists:d1111 -all", Pass, ErrMatchedExists},
 		{"v=spf1 redirect=", PermError, ErrInvalidDomain},
+
+		// Unrecognized modifiers are ignored, so evaluation continues.
+		// https://tools.ietf.org/html/rfc7208#section-6
+		{"v=spf1 ra=postmaster -all", Fail, ErrMatchedAll},
+		{"v=spf1 rp=100 -all", Fail, ErrMatchedAll},
+		{"v=spf1 rr=e ip4:1.1.1.1 -all", Pass, ErrMatchedIP},
+		{"v=spf1 moo.cow-far_out=man:dog/cat ip4:1.1.1.1 -all", Pass, ErrMatchedIP},
+		{"v=spf1 x=%{d} -all", Fail, ErrMatchedAll},
+		{"v=spf1 a1=x a2=y a3=z -all", Fail, ErrMatchedAll},
+
+		// But the name has to be valid: it starts with a letter, and the
+		// "=" comes before any ":" or "/".
+		{"v=spf1 moo.cow/far_out=man -all", PermError, ErrUnknownField},
+		{"v=spf1 moo.cow:far_out=man -all", PermError, ErrUnknownField},
+		{"v=spf1 1abc=x -all", PermError, ErrUnknownField},
+		{"v=spf1 =x -all", PermError, ErrUnknownField},
+
+		// Modifiers take no qualifier, so this is not one.
+		{"v=spf1 -ra=postmaster all", PermError, ErrUnknownField},
 	}
 
 	dns.Ip["d1111"] = []net.IP{ip1111}
diff --git a/testdata/rfc4408-tests.yml b/testdata/rfc4408-tests.yml
index d39dfd8..898d43c 100644
--- a/testdata/rfc4408-tests.yml
+++ b/testdata/rfc4408-tests.yml
@@ -379,7 +379,6 @@ tests:
     host: 1.2.3.4
     mailfrom: foo@t2.example.com
     result: pass
-    skip: We don't enforce the domain charset.
   modifier-charset-bad1:
     description: >-
       '=' character immediately after the name and before any ":" or "/"
diff --git a/testdata/rfc7208-tests.yml b/testdata/rfc7208-tests.yml
index 7cdac46..d730242 100644
--- a/testdata/rfc7208-tests.yml
+++ b/testdata/rfc7208-tests.yml
@@ -419,7 +419,6 @@ tests:
     host: 1.2.3.4
     mailfrom: foo@t2.example.com
     result: pass
-    skip: We don't enforce the domain charset.
   modifier-charset-bad1:
     description: >-
       '=' character immediately after the name and before any ":" or "/"