| author | Alberto Bertogli
<albertito@blitiri.com.ar> 2026-08-21 21:01:56 UTC |
| committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2026-08-22 09:01:21 UTC |
| parent | 2868f0bfddcd7e785526155486bb06469418a45b |
| 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 "/"