This is an automated email from the ASF dual-hosted git repository. quantranhong1999 pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-jspf.git
commit 0eafb7af1c9915d5b96a28f370711f166c0020b8 Author: Benoit TELLIER <[email protected]> AuthorDate: Wed Oct 28 22:11:22 2026 +0100 [FIX] Escape the domain written in the Received-SPF header comment The current domain comes from DNS (redirect= target). Its domain-spec allows "(", ")", "\" and ";", so a sender could close the comment and add its own key=value fields (client-ip=...) to the header. Escape "(", ")" and "\" as RFC 5322 quoted-pairs and drop characters outside ctext. Co-Authored-By: Claude Opus 5.5 <[email protected]> --- .../org/apache/james/jspf/executor/SPFResult.java | 30 +++++++++++++++++++--- .../org/apache/james/jspf/SPFExecutorBaseTest.java | 19 ++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/resolver/src/main/java/org/apache/james/jspf/executor/SPFResult.java b/resolver/src/main/java/org/apache/james/jspf/executor/SPFResult.java index 5ebf96c..c274b11 100644 --- a/resolver/src/main/java/org/apache/james/jspf/executor/SPFResult.java +++ b/resolver/src/main/java/org/apache/james/jspf/executor/SPFResult.java @@ -101,21 +101,21 @@ public class SPFResult { if (result.equals(SPFErrorConstants.PASS_CONV)) { headerText.append(result + " (spfCheck: domain of " - + spfData.getCurrentDomain() + " designates " + + escapeComment(spfData.getCurrentDomain()) + " designates " + spfData.getIpAddress() + " as permitted sender) "); } else if (result.equals(SPFErrorConstants.FAIL_CONV)) { headerText.append(result + " (spfCheck: domain of " - + spfData.getCurrentDomain() + " does not designate " + + escapeComment(spfData.getCurrentDomain()) + " does not designate " + spfData.getIpAddress() + " as permitted sender) "); } else if (result.equals(SPFErrorConstants.NEUTRAL_CONV) || result.equals(SPFErrorConstants.NONE_CONV)) { headerText.append(result + " (spfCheck: " + spfData.getIpAddress() + " is neither permitted nor denied by domain of " - + spfData.getCurrentDomain() + ") "); + + escapeComment(spfData.getCurrentDomain()) + ") "); } else if (result.equals(SPFErrorConstants.SOFTFAIL_CONV)) { headerText.append(result + " (spfCheck: transitioning domain of " - + spfData.getCurrentDomain() + " does not designate " + + escapeComment(spfData.getCurrentDomain()) + " does not designate " + spfData.getIpAddress() + " as permitted sender) "); } else if (result.equals(SPFErrorConstants.PERM_ERROR_CONV)) { headerText.append(result @@ -139,6 +139,28 @@ public class SPFResult { return headerTextAsString; } + /** + * Escape a value written inside the comment of the header: "(", ")" and + * "\\" become RFC 5322 quoted-pairs, and characters that are neither + * ctext nor space are dropped, so that the value cannot close the comment. + * + * @param value The value to escape, possibly taken from DNS + */ + private static String escapeComment(String value) { + if (value == null) { + return null; + } + StringBuilder escaped = new StringBuilder(value.length()); + for (char c : value.toCharArray()) { + if (c == '(' || c == ')' || c == '\\') { + escaped.append('\\').append(c); + } else if (c >= ' ' && c <= '~') { + escaped.append(c); + } + } + return escaped.toString(); + } + /** * Get the result string * 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 acdc376..8042c22 100644 --- a/resolver/src/test/java/org/apache/james/jspf/SPFExecutorBaseTest.java +++ b/resolver/src/test/java/org/apache/james/jspf/SPFExecutorBaseTest.java @@ -261,4 +261,23 @@ public abstract class SPFExecutorBaseTest { result.getHeader()); assertEquals(SPFErrorConstants.PASS_CONV, result.getResult()); } + + @Test + public void headerCommentShouldNotBeClosedByTheRedirectDomain() throws IOException { + String hostname = "headerCommentShouldNotBeClosedByTheRedirectDomain.spfexecutor.fake"; + String target = "x)client-ip=203.0.113.9;(." + hostname; + String ip = "192.0.2.127"; + + FakeResolver fakeResolver = new FakeResolver(); + fakeResolver.addRecord(new TXTRecord(Name.fromString(hostname + "."), DClass.IN, 30L, "v=spf1 redirect=" + target)); + fakeResolver.addRecord(new TXTRecord(Name.fromString(target + "."), DClass.IN, 30L, "v=spf1 -all")); + + SPF spf = createCustomSPF(new DNSServiceXBillImpl(fakeResolver)); + SPFResult result = spf.checkSPF(ip, "a_user@" + hostname, hostname); + assertEquals(SPFErrorConstants.FAIL_CONV, result.getResult()); + assertEquals(String.format( + "Received-SPF: %3$s (spfCheck: domain of x\\)client-ip=203.0.113.9;\\(.%2$s does not designate %1$s as permitted sender) client-ip=%1$s; envelope-from=a_user@%2$s; helo=%2$s;", + ip, hostname, SPFErrorConstants.FAIL_CONV), + result.getHeader()); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
