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();
