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]

Reply via email to