git » spf » commit c7ffeee

ptr: Fix DNS count check on repeated terms

author Alberto Bertogli
2026-08-16 13:32:51 UTC
committer Alberto Bertogli
2026-08-16 13:32:51 UTC
parent 42e35196b3997a40337fed3c1fe37b8cd8e43c21

ptr: Fix DNS count check on repeated terms

Each "ptr" term counts against the lookup limit, even if we already have
the names from a previous one and don't need to query DNS again: the RFC
limits the number of terms that cause DNS lookups, not the number of
queries we end up making:

> The following terms cause DNS queries: the "include", "a", "mx",
> "ptr", and "exists" mechanisms, and the "redirect" modifier.  SPF
> implementations MUST limit the total number of those terms to 10
> during SPF evaluation, to avoid unreasonable load on the DNS.  If this
> limit is exceeded, the implementation MUST return "permerror".

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

Today, we resolve each address only once per appeareance (so `ptr:a.com
ptr:a.com` does only one actual DNS lookup), but we only count it once
for the purpose of DNS counting limits, which is technically incorrect.

This patch fixes the problem by counting each `ptr` term, regardless of
whether a DNS lookup was actually needed.

This is similar to what was done in commit
5a188ab835d1e8da402a8c6966a57dcd9dd81354 for the `mx` term.

spf.go +9 -3
testdata/blitirispf-tests.yml +48 -0

diff --git a/spf.go b/spf.go
index 4ed1539..06a90db 100644
--- a/spf.go
+++ b/spf.go
@@ -552,10 +552,16 @@ func (r *resolution) ptrField(res Result, field, domain string) (bool, Result, e
 		return true, PermError, ErrInvalidDomain
 	}
 
+	// Each "ptr" term counts against the lookup limit, even if we already have
+	// the names from a previous one and don't need to query DNS again: the RFC
+	// limits the number of terms that cause DNS lookups, not the number of
+	// queries we end up making.
+	// https://tools.ietf.org/html/rfc7208#section-4.6.4
+	if err := r.countLookup(); err != nil {
+		return true, PermError, err
+	}
+
 	if r.ipNames == nil {
-		if err := r.countLookup(); err != nil {
-			return true, PermError, err
-		}
 		r.ipNames = []string{}
 		ns, err := r.resolver.LookupAddr(r.ctx, r.ip.String())
 		if verr := r.countVoidLookup(len(ns), err); verr != nil {
diff --git a/testdata/blitirispf-tests.yml b/testdata/blitirispf-tests.yml
index cc2c002..914e353 100644
--- a/testdata/blitirispf-tests.yml
+++ b/testdata/blitirispf-tests.yml
@@ -413,6 +413,54 @@ zonedata:
     - MX: [2, blah2]
     - MX: [3, blah3]
 ---
+description: PTR resolution limits
+tests:
+  ptr-resolution-10-terms:
+    description: |
+      Check that a resolution with 10 "ptr" terms works, because it's within
+      the limit, and that the terms after them are still evaluated. The
+      address is only resolved once and the names reused, but each term still
+      counts against the limit.
+    mailfrom: "foo@ptr10"
+    host: 1.2.3.4
+    result: pass
+  ptr-resolution-11-terms:
+    description: |
+      Check that a resolution with 11 "ptr" terms causes a permerror due to
+      exceeding lookup limits, even though only one DNS lookup is performed.
+    mailfrom: "foo@ptr11"
+    host: 1.2.3.4
+    result: permerror
+  ptr-resolution-mixed-terms:
+    description: |
+      Check that "ptr" terms are counted individually when mixed with other
+      terms that cause DNS lookups: here the second "ptr" is the 11th term,
+      and takes the resolution over the limit.
+    mailfrom: "foo@ptrmixed"
+    host: 1.2.3.4
+    result: permerror
+zonedata:
+  ptr10:
+    - SPF: v=spf1 ptr:nomatch.com ptr:nomatch.com ptr:nomatch.com
+                  ptr:nomatch.com ptr:nomatch.com ptr:nomatch.com
+                  ptr:nomatch.com ptr:nomatch.com ptr:nomatch.com
+                  ptr:nomatch.com ip4:1.2.3.4 -all
+  ptr11:
+    - SPF: v=spf1 ptr:nomatch.com ptr:nomatch.com ptr:nomatch.com
+                  ptr:nomatch.com ptr:nomatch.com ptr:nomatch.com
+                  ptr:nomatch.com ptr:nomatch.com ptr:nomatch.com
+                  ptr:nomatch.com ptr:nomatch.com -all
+  ptrmixed:
+    - SPF: v=spf1 a:nomatch a:nomatch a:nomatch a:nomatch a:nomatch
+                  a:nomatch a:nomatch a:nomatch a:nomatch
+                  ptr:nomatch.com ptr:nomatch.com -all
+  nomatch:
+    - A: 10.0.0.1
+  ptrhost.com:
+    - A: 1.2.3.4
+  4.3.2.1.in-addr.arpa:
+    - PTR: ptrhost.com
+---
 description: NXDOMAIN tests
 tests:
   one-mx-not-found: