This is an automated email from the ASF dual-hosted git repository. chibenwa pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-jspf.git
commit 080bbc7782b9f17f546d4dd2898fd60f638faff4 Author: Quan Tran <[email protected]> AuthorDate: Tue Nov 3 15:48:47 2026 +0700 [FIX] ptr mechanism: compare the domain names whatever their case ptr matched a validated name against the target domain with equals and endsWith, so as text: with the case and the trailing dot of the domain as written, and not label by label. For a client whose validated name is mail.example.com, "v=spf1 ptr:EXAMPLE.COM -all" failed, and so did "v=spf1 ptr -all" for the sender domain EXAMPLE.COM or example.com. (with the dot of a fully qualified name), which pass as example.com. And a validated name whose first label holds a dot, mail\.example.com, a sibling of example.com, passed as one of its subdomains. Compare them as domain names, label by label, whatever the case and a trailing dot (RFC 4343, RFC 7208 5.5). A root target still matches no name. --- .../org/apache/james/jspf/terms/PTRMechanism.java | 21 +++++++++- .../org/apache/james/jspf/SPFExecutorBaseTest.java | 48 ++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/resolver/src/main/java/org/apache/james/jspf/terms/PTRMechanism.java b/resolver/src/main/java/org/apache/james/jspf/terms/PTRMechanism.java index 20e9e82..5ac51d3 100644 --- a/resolver/src/main/java/org/apache/james/jspf/terms/PTRMechanism.java +++ b/resolver/src/main/java/org/apache/james/jspf/terms/PTRMechanism.java @@ -36,6 +36,8 @@ import org.apache.james.jspf.core.exceptions.NoneException; import org.apache.james.jspf.core.exceptions.PermErrorException; import org.apache.james.jspf.core.exceptions.TempErrorException; import org.apache.james.jspf.core.exceptions.TimeoutException; +import org.xbill.DNS.Name; +import org.xbill.DNS.TextParseException; import java.util.List; @@ -93,6 +95,22 @@ public class PTRMechanism extends GenericMechanism implements DNSServiceEnabled, */ public static final String REGEX = "[pP][tT][rR]" + "(?:\\:" + SPFTermsRegexps.DOMAIN_SPEC_REGEX + ")?"; + + /** + * @param validated a validated domain name + * @param target the target domain + * @return true if the validated name is the target or a subdomain of it, label by label and + * whatever the case and a trailing dot (RFC 4343, RFC 7208 5.5) + */ + private static boolean isSubdomain(String validated, String target) { + try { + Name targetName = Name.fromString(target, Name.root); + // the root is no target: every name is under it + return !targetName.equals(Name.root) && Name.fromString(validated, Name.root).subdomain(targetName); + } catch (TextParseException e) { + return false; + } + } private DNSService dnsService; @@ -159,8 +177,7 @@ public class PTRMechanism extends GenericMechanism implements DNSServiceEnabled, // Added the IPAddr parsing/toString to have matching in IPV6 multiple ways to if (IPAddr.getAddress((String) aList.get(j)).getIPAddress().equals(IPAddr.getAddress(spfSession.getIpAddress()).getIPAddress())) { - if (compareDomain.equals(host) - || compareDomain.endsWith("." + host)) { + if (isSubdomain(compareDomain, host)) { spfSession.setAttribute(Directive.ATTRIBUTE_MECHANISM_RESULT, Boolean.TRUE); return null; } diff --git a/resolver/src/test/java/org/apache/james/jspf/SPFExecutorBaseTest.java b/resolver/src/test/java/org/apache/james/jspf/SPFExecutorBaseTest.java index 11cc622..613a99e 100644 --- a/resolver/src/test/java/org/apache/james/jspf/SPFExecutorBaseTest.java +++ b/resolver/src/test/java/org/apache/james/jspf/SPFExecutorBaseTest.java @@ -16,6 +16,7 @@ import org.xbill.DNS.DClass; import org.xbill.DNS.Lookup; import org.xbill.DNS.MXRecord; import org.xbill.DNS.Name; +import org.xbill.DNS.PTRRecord; import org.xbill.DNS.Resolver; import org.xbill.DNS.SPFRecord; import org.xbill.DNS.SimpleResolver; @@ -31,6 +32,7 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.ThreadLocalRandom; @@ -61,6 +63,18 @@ public abstract class SPFExecutorBaseTest { return fakeResolver; } + // hostname has "v=spf1 <ptrTerm> -all", and 1.2.3.4 the validated name <ptrName> + private static FakeResolver ptrOfTheClient(String hostname, String ptrTerm, String ptrName) throws IOException { + FakeResolver fakeResolver = new FakeResolver(); + fakeResolver.addRecord(new TXTRecord(Name.fromString(hostname + "."), DClass.IN, 30L, + "v=spf1 " + ptrTerm + " -all")); + fakeResolver.addRecord(new PTRRecord(Name.fromString("4.3.2.1.in-addr.arpa."), DClass.IN, 30L, + Name.fromString(ptrName))); + fakeResolver.addRecord(new ARecord(Name.fromString(ptrName), DClass.IN, 30L, + InetAddress.getByName("1.2.3.4"))); + return fakeResolver; + } + protected abstract SPF createSPF(); protected abstract SPF createCustomSPF(DNSService dnsService); @@ -618,6 +632,40 @@ public abstract class SPFExecutorBaseTest { assertEquals(SPFErrorConstants.FAIL_CONV, result.getResult()); } + @Test(timeout = 10000) + public void ptrShouldMatchADomainInUpperCase() throws IOException { + String hostname = "ptrcase.spfexecutor.fake"; + FakeResolver fakeResolver = ptrOfTheClient(hostname, "ptr:" + hostname.toUpperCase(Locale.ROOT), + "mail." + hostname + "."); + SPF spf = createCustomSPF(new DNSServiceXBillImpl(fakeResolver)); + + SPFResult result = spf.checkSPF("1.2.3.4", "a_user@" + hostname, hostname); + + assertEquals(SPFErrorConstants.PASS_CONV, result.getResult()); + } + + @Test(timeout = 10000) + public void ptrShouldMatchASenderDomainWithATrailingDot() throws IOException { + String hostname = "ptrdot.spfexecutor.fake"; + SPF spf = createCustomSPF(new DNSServiceXBillImpl(ptrOfTheClient(hostname, "ptr", "mail." + hostname + "."))); + + SPFResult result = spf.checkSPF("1.2.3.4", "a_user@" + hostname + ".", hostname); + + assertEquals(SPFErrorConstants.PASS_CONV, result.getResult()); + } + + @Test(timeout = 10000) + public void ptrShouldNotMatchALabelHoldingADotAsASubdomain() throws IOException { + String hostname = "ptrlabel.spfexecutor.fake"; + // the labels "mail.ptrlabel", "spfexecutor" and "fake": a sibling of hostname, not a subdomain + FakeResolver fakeResolver = ptrOfTheClient(hostname, "ptr", "mail\\." + hostname + "."); + SPF spf = createCustomSPF(new DNSServiceXBillImpl(fakeResolver)); + + SPFResult result = spf.checkSPF("1.2.3.4", "a_user@" + hostname, hostname); + + assertEquals(SPFErrorConstants.FAIL_CONV, result.getResult()); + } + @Test(timeout = 10000) public void shouldReturnPermErrorOnAIp4CidrOverflowingAnInt() throws IOException { SPFResult result = checkRecord("aIp4CidrOverflow.spfexecutor.fake", "v=spf1 a/4294967296 -all", "1.2.3.4"); --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
