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 b83aa8e62e03e3e770f3263f0220b61fd96c8402 Author: Quan Tran <[email protected]> AuthorDate: Wed Nov 4 21:45:10 2026 +0700 [FIX] InitialChecksPolicy: none for a malformed sender domain The sender domain was only checked to hold a dot, labels of at most 63 characters, and to be read by dnsjava as a DNS name in text form, whose escapes make it another name: MAIL FROM a_user@escape\d.example.com was checked as escaped.example.com, and got its result. A sender domain comes from the SMTP session, where it is a host name of letters, digits and hyphens (RFC 5321 4.1.2), an internationalized one written as A-labels (RFC 7208 4.3), so such a domain is malformed, and the result is none (RFC 7208 4.3). Return none for a sender domain holding a character other than a letter, a digit, "-", "." or "_", seen in host names, also when a local policy gave the record: an override entry for escaped.example.com, read as a DNS name too, matched escape\d.example.com. usage.adoc says that the domains are given as host names. A test of the local policies sent such a MAIL FROM: its result is now none, still not the pass of the override. --- docs/modules/ROOT/pages/usage.adoc | 4 +++ .../james/jspf/policies/InitialChecksPolicy.java | 15 ++++++++ .../org/apache/james/jspf/SPFExecutorBaseTest.java | 41 +++++++++++++++++++++- 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/usage.adoc b/docs/modules/ROOT/pages/usage.adoc index 013d21a..332f9f7 100644 --- a/docs/modules/ROOT/pages/usage.adoc +++ b/docs/modules/ROOT/pages/usage.adoc @@ -48,6 +48,10 @@ System.out.println(result.getResult()); <2> The envelope sender, as provided in `MAIL FROM`. <3> The host name provided in `HELO` / `EHLO`. +Give the domains as host names: an internationalized domain in A-labels (`xn--`), as RFC 7208 +4.3 requires. A sender domain holding another character (a U-label, a space, a backslash) is +malformed, and the result is `none`. + `SPF` instances are meant to be created once and reused for many checks. == Interpreting the result diff --git a/resolver/src/main/java/org/apache/james/jspf/policies/InitialChecksPolicy.java b/resolver/src/main/java/org/apache/james/jspf/policies/InitialChecksPolicy.java index 11424b2..19daf53 100644 --- a/resolver/src/main/java/org/apache/james/jspf/policies/InitialChecksPolicy.java +++ b/resolver/src/main/java/org/apache/james/jspf/policies/InitialChecksPolicy.java @@ -37,6 +37,15 @@ import org.xbill.DNS.TextParseException; * so it should be added as the last filter. */ public final class InitialChecksPolicy implements SPFChecker { + /** + * @param domain a domain of the SMTP session + * @return true if it holds only the characters of a host name: letters, digits, "-" and ".", + * and "_", seen in names; an internationalized name is given as A-labels (RFC 7208 4.3) + */ + private static boolean holdsOnlyHostNameCharacters(String domain) { + return domain.chars().allMatch(c -> (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9') || c == '-' || c == '.' || c == '_'); + } /** * @see org.apache.james.jspf.core.SPFChecker#checkSPF(org.apache.james.jspf.core.SPFSession) @@ -44,6 +53,12 @@ public final class InitialChecksPolicy implements SPFChecker { public DNSLookupContinuation checkSPF(SPFSession spfData) throws PermErrorException, TempErrorException, NeutralException, NoneException { + // not read as a DNS name, whose escapes would make it another domain: "escape\d" is no "escaped"; + // checked also when a local policy gave the record, as it matches the domain read so + if (!holdsOnlyHostNameCharacters(spfData.getSenderDomain())) { + throw new NoneException("Sender domain " + spfData.getSenderDomain() + " is malformed."); + } + SPF1Record res = (SPF1Record) spfData.getAttribute(SPF1Utils.ATTRIBUTE_SPF1_RECORD); if (res == 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 c5f050f..bdec8fb 100644 --- a/resolver/src/test/java/org/apache/james/jspf/SPFExecutorBaseTest.java +++ b/resolver/src/test/java/org/apache/james/jspf/SPFExecutorBaseTest.java @@ -661,7 +661,8 @@ public abstract class SPFExecutorBaseTest { SPFResult result = spf.checkSPF("192.0.2.10", "a_user@" + hostname, hostname); - assertEquals(SPFErrorConstants.FAIL_CONV, result.getResult()); + // not the pass of the override: the sender domain, holding a backslash, is malformed (RFC 7208 4.3) + assertEquals(SPFErrorConstants.NONE_CONV, result.getResult()); } @Test(timeout = 10000) @@ -769,6 +770,44 @@ public abstract class SPFExecutorBaseTest { assertEquals(SPFErrorConstants.FAIL_CONV, result.getResult()); } + @Test(timeout = 10000) + public void shouldReturnNoneForASenderDomainWithACharacterNoHostNameHolds() throws IOException { + String hostname = "escaped.spfexecutor.fake"; + FakeResolver fakeResolver = new FakeResolver(); + fakeResolver.addRecord(new TXTRecord(Name.fromString(hostname + "."), DClass.IN, 30L, "v=spf1 +all")); + + SPF spf = createCustomSPF(new DNSServiceXBillImpl(fakeResolver)); + // read as a DNS name, escape\d is escaped + SPFResult result = spf.checkSPF("192.0.2.10", "a_user@escape\\d.spfexecutor.fake", "mail.spfexecutor.fake"); + + // RFC 7208 4.3: a malformed domain gives none + assertEquals(SPFErrorConstants.NONE_CONV, result.getResult()); + } + + @Test(timeout = 10000) + public void anOverrideShouldNotApplyToAMalformedSenderDomain() throws IOException { + SPF spf = createCustomSPF(new DNSServiceXBillImpl(new FakeResolver(AnsweringResolver.withRcode(Rcode.NXDOMAIN)))); + spf.getOverridePolicy().addEntry("escaped.spfexecutor.fake", "v=spf1 +all"); + + // read as a DNS name, escape\d is escaped: the domain of the override entry + SPFResult result = spf.checkSPF("192.0.2.10", "a_user@escape\\d.spfexecutor.fake", "mail.spfexecutor.fake"); + + // RFC 7208 4.3: a malformed domain gives none + assertEquals(SPFErrorConstants.NONE_CONV, result.getResult()); + } + + @Test(timeout = 10000) + public void shouldCheckASenderDomainWithAnUnderscore() throws IOException { + String hostname = "_underscore.spfexecutor.fake"; + FakeResolver fakeResolver = new FakeResolver(); + fakeResolver.addRecord(new TXTRecord(Name.fromString(hostname + "."), DClass.IN, 30L, "v=spf1 +all")); + + SPF spf = createCustomSPF(new DNSServiceXBillImpl(fakeResolver)); + SPFResult result = spf.checkSPF("192.0.2.10", "a_user@" + hostname, hostname); + + assertEquals(SPFErrorConstants.PASS_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]
