This is an automated email from the ASF dual-hosted git repository.

reta pushed a commit to branch 3.6.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git

commit 5102598a404a9a0b3e49fe9cd4e8fb3c5b6616d0
Author: Javid Khan <[email protected]>
AuthorDate: Tue Jul 28 17:00:16 2026 +0530

    match saml sso issuer exactly instead of by prefix (#3281)
    
    * match saml sso issuer exactly instead of by prefix
    
    * restrict saml sso issuer prefix match to same origin
    
    keep prefix compatibility for the entityID-as-endpoint case but require
    url-based issuer values to share the scheme, host and port of the
    configured issuer idp, and add an enforceStrictIssuerMatch flag for
    deployments that want exact matching only
    
    Signed-off-by: Javid Khan <[email protected]>
    
    ---------
    
    Signed-off-by: Javid Khan <[email protected]>
    (cherry picked from commit dd9af5c9ae419ac410bd308c49a3e09706232a30)
---
 .../saml/sso/SAMLSSOResponseValidator.java         | 68 +++++++++++++++++-
 .../saml/sso/SAMLSSOResponseValidatorTest.java     | 82 ++++++++++++++++++++++
 2 files changed, 148 insertions(+), 2 deletions(-)

diff --git 
a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidator.java
 
b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidator.java
index 3f6de430fee..5f779a74a18 100644
--- 
a/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidator.java
+++ 
b/rt/rs/security/sso/saml/src/main/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidator.java
@@ -18,6 +18,7 @@
  */
 package org.apache.cxf.rs.security.saml.sso;
 
+import java.net.URI;
 import java.time.Instant;
 import java.util.List;
 import java.util.logging.Logger;
@@ -47,6 +48,7 @@ public class SAMLSSOResponseValidator {
     private boolean enforceResponseSigned;
     private boolean enforceAssertionsSigned = true;
     private boolean enforceKnownIssuer = true;
+    private boolean enforceStrictIssuerMatch;
     private TokenReplayCache<String> replayCache;
 
     /**
@@ -64,6 +66,15 @@ public class SAMLSSOResponseValidator {
         this.enforceKnownIssuer = enforceKnownIssuer;
     }
 
+    /**
+     * Require the Issuer of the received Response/Assertion to match the 
configured Issuer IDP
+     * exactly, rather than allowing a same-origin prefix. The default is 
false, which keeps
+     * backwards compatibility for deployments whose entityID is a prefix of 
the configured value.
+     */
+    public void setEnforceStrictIssuerMatch(boolean enforceStrictIssuerMatch) {
+        this.enforceStrictIssuerMatch = enforceStrictIssuerMatch;
+    }
+
     /**
      * Validate a SAML 2 Protocol Response
      * @param samlResponse
@@ -172,8 +183,8 @@ public class SAMLSSOResponseValidator {
             return;
         }
 
-        // Issuer value must match (be contained in) Issuer IDP
-        if (enforceKnownIssuer && (issuer.getValue() == null || 
!issuerIDP.startsWith(issuer.getValue()))) {
+        // Issuer value must match the configured Issuer IDP
+        if (enforceKnownIssuer && !matchesKnownIssuer(issuer.getValue())) {
             LOG.warning("Issuer value: " + issuer.getValue() + " does not 
match issuer IDP: "
                 + issuerIDP);
             throw new 
WSSecurityException(WSSecurityException.ErrorCode.FAILURE, 
"invalidSAMLsecurity");
@@ -188,6 +199,59 @@ public class SAMLSSOResponseValidator {
         }
     }
 
+    private boolean matchesKnownIssuer(String issuerValue) {
+        if (issuerValue == null || issuerIDP == null) {
+            return false;
+        }
+
+        if (issuerIDP.equals(issuerValue)) {
+            return true;
+        }
+
+        // Strict matching only accepts an exact match with the configured 
Issuer IDP
+        if (enforceStrictIssuerMatch || !issuerIDP.startsWith(issuerValue)) {
+            return false;
+        }
+
+        // Keep prefix compatibility, but for URL-based IdP values require a 
URL-based issuer on
+        // the same scheme/host/port to avoid accepting arbitrary short 
prefixes.
+        URI issuerIdpUri = toUri(issuerIDP);
+        if (isHierarchicalAbsoluteUri(issuerIdpUri)) {
+            URI issuerUri = toUri(issuerValue);
+            return isHierarchicalAbsoluteUri(issuerUri)
+                && 
issuerIdpUri.getScheme().equalsIgnoreCase(issuerUri.getScheme())
+                && issuerIdpUri.getHost().equalsIgnoreCase(issuerUri.getHost())
+                && getEffectivePort(issuerIdpUri) == 
getEffectivePort(issuerUri);
+        }
+
+        return true;
+    }
+
+    private URI toUri(String value) {
+        try {
+            return URI.create(value);
+        } catch (IllegalArgumentException ex) {
+            return null;
+        }
+    }
+
+    private boolean isHierarchicalAbsoluteUri(URI uri) {
+        return uri != null && uri.isAbsolute() && uri.getHost() != null;
+    }
+
+    private int getEffectivePort(URI uri) {
+        if (uri.getPort() != -1) {
+            return uri.getPort();
+        }
+        if ("http".equalsIgnoreCase(uri.getScheme())) {
+            return 80;
+        }
+        if ("https".equalsIgnoreCase(uri.getScheme())) {
+            return 443;
+        }
+        return -1;
+    }
+
     /**
      * Validate the Subject (of an Authentication Statement).
      */
diff --git 
a/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidatorTest.java
 
b/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidatorTest.java
index b5dc509448d..9bc3e695ca2 100644
--- 
a/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidatorTest.java
+++ 
b/rt/rs/security/sso/saml/src/test/java/org/apache/cxf/rs/security/saml/sso/SAMLSSOResponseValidatorTest.java
@@ -323,6 +323,88 @@ public class SAMLSSOResponseValidatorTest {
         }
     }
 
+    @org.junit.Test
+    public void testResponseIssuerShortPrefixOfConfiguredIssuer() throws 
Exception {
+        SubjectConfirmationDataBean subjectConfirmationData = new 
SubjectConfirmationDataBean();
+        subjectConfirmationData.setAddress("http://apache.org";);
+        subjectConfirmationData.setInResponseTo("12345");
+        
subjectConfirmationData.setNotAfter(Instant.now().plus(Duration.ofMinutes(5)));
+        subjectConfirmationData.setRecipient("http://recipient.apache.org";);
+
+        Response response = createResponse(subjectConfirmationData);
+        // An arbitrary short prefix on a different host must not be accepted
+        
response.setIssuer(SAML2PResponseComponentBuilder.createIssuer("http://cxf";));
+
+        // Validate the Response
+        SAMLSSOResponseValidator validator = new SAMLSSOResponseValidator();
+        validator.setEnforceAssertionsSigned(false);
+        validator.setIssuerIDP("http://cxf.apache.org/issuer";);
+        validator.setAssertionConsumerURL("http://recipient.apache.org";);
+        validator.setClientAddress("http://apache.org";);
+        validator.setRequestId("12345");
+        validator.setSpIdentifier("http://service.apache.org";);
+        try {
+            validator.validateSamlResponse(response, false);
+            fail("Expected failure on issuer that only matches a short prefix 
of the configured issuer");
+        } catch (WSSecurityException ex) {
+            // expected
+        }
+    }
+
+    @org.junit.Test
+    public void testResponseIssuerSameOriginPrefixAccepted() throws Exception {
+        SubjectConfirmationDataBean subjectConfirmationData = new 
SubjectConfirmationDataBean();
+        subjectConfirmationData.setAddress("http://apache.org";);
+        subjectConfirmationData.setInResponseTo("12345");
+        
subjectConfirmationData.setNotAfter(Instant.now().plus(Duration.ofMinutes(5)));
+        subjectConfirmationData.setRecipient("http://recipient.apache.org";);
+
+        Response response = createResponse(subjectConfirmationData);
+        // A prefix on the same scheme/host/port is the legitimate 
entityID-of-endpoint case
+        
response.setIssuer(SAML2PResponseComponentBuilder.createIssuer("http://cxf.apache.org";));
+
+        // Validate the Response
+        SAMLSSOResponseValidator validator = new SAMLSSOResponseValidator();
+        validator.setEnforceAssertionsSigned(false);
+        validator.setIssuerIDP("http://cxf.apache.org/issuer";);
+        validator.setAssertionConsumerURL("http://recipient.apache.org";);
+        validator.setClientAddress("http://apache.org";);
+        validator.setRequestId("12345");
+        validator.setSpIdentifier("http://service.apache.org";);
+
+        SSOValidatorResponse validateSamlResponse = 
validator.validateSamlResponse(response, false);
+        assertEquals(response.getID(), validateSamlResponse.getResponseId());
+    }
+
+    @org.junit.Test
+    public void testResponseIssuerStrictMatchRejectsPrefix() throws Exception {
+        SubjectConfirmationDataBean subjectConfirmationData = new 
SubjectConfirmationDataBean();
+        subjectConfirmationData.setAddress("http://apache.org";);
+        subjectConfirmationData.setInResponseTo("12345");
+        
subjectConfirmationData.setNotAfter(Instant.now().plus(Duration.ofMinutes(5)));
+        subjectConfirmationData.setRecipient("http://recipient.apache.org";);
+
+        Response response = createResponse(subjectConfirmationData);
+        // Same-origin prefix, but strict matching must require an exact match
+        
response.setIssuer(SAML2PResponseComponentBuilder.createIssuer("http://cxf.apache.org";));
+
+        // Validate the Response
+        SAMLSSOResponseValidator validator = new SAMLSSOResponseValidator();
+        validator.setEnforceAssertionsSigned(false);
+        validator.setEnforceStrictIssuerMatch(true);
+        validator.setIssuerIDP("http://cxf.apache.org/issuer";);
+        validator.setAssertionConsumerURL("http://recipient.apache.org";);
+        validator.setClientAddress("http://apache.org";);
+        validator.setRequestId("12345");
+        validator.setSpIdentifier("http://service.apache.org";);
+        try {
+            validator.validateSamlResponse(response, false);
+            fail("Expected failure on prefix issuer when strict matching is 
enforced");
+        } catch (WSSecurityException ex) {
+            // expected
+        }
+    }
+
     @org.junit.Test
     public void testMissingAuthnStatement() throws Exception {
         SubjectConfirmationDataBean subjectConfirmationData = new 
SubjectConfirmationDataBean();

Reply via email to