From 7fc97c6ee23864fbf17faa112c5fda0012f58722 Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Mon, 1 Jun 2026 14:05:44 -0600 Subject: [PATCH] Select Most Specific RDN in X500 Principal Extractor SubjectX500PrincipalExtractor now returns the left-most, most-specific matching RDN value, matching both convention and SubjectDnX509PrincipalExtractor. Since LdapName#getRdns lists RDNs most-significant first, the extractor reads them in reverse so that a subject with more than one matching attribute (e.g. two CNs) resolves to the most specific value. Closes gh-19254 Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com> --- .../x509/SubjectX500PrincipalExtractor.java | 7 +++- .../SubjectDnX509PrincipalExtractorTests.java | 7 ++++ .../SubjectX500PrincipalExtractorTests.java | 8 +++++ .../preauth/x509/X509TestUtils.java | 34 +++++++++++++++++++ 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/web/src/main/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractor.java b/web/src/main/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractor.java index c955fb72fe..df8ad5cca2 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractor.java +++ b/web/src/main/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractor.java @@ -17,6 +17,8 @@ package org.springframework.security.web.authentication.preauth.x509; import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import javax.naming.InvalidNameException; @@ -72,7 +74,10 @@ public final class SubjectX500PrincipalExtractor implements X509PrincipalExtract private List getDns(String subjectDn) { try { - return new LdapName(subjectDn).getRdns(); + // read most-specific first, see gh-19254 + List rdns = new ArrayList<>(new LdapName(subjectDn).getRdns()); + Collections.reverse(rdns); + return rdns; } catch (InvalidNameException ex) { throw new BadCredentialsException("Failed to parse client certificate", ex); diff --git a/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/SubjectDnX509PrincipalExtractorTests.java b/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/SubjectDnX509PrincipalExtractorTests.java index 1dbdc3d9f1..c4a60f1588 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/SubjectDnX509PrincipalExtractorTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/SubjectDnX509PrincipalExtractorTests.java @@ -74,6 +74,13 @@ public class SubjectDnX509PrincipalExtractorTests { assertThat(principal).isEqualTo("Duke"); } + // gh-19254 + @Test + public void defaultCNPatternReturnsMostSpecificPrincipalWhenMultipleCns() throws Exception { + Object principal = this.extractor.extractPrincipal(X509TestUtils.buildTestCertificateWithMultipleCns()); + assertThat(principal).isEqualTo("alice"); + } + @Test public void setMessageSourceWhenNullThenThrowsException() { assertThatIllegalArgumentException().isThrownBy(() -> this.extractor.setMessageSource(null)); diff --git a/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractorTests.java b/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractorTests.java index d5bc6635b0..5c53202290 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractorTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractorTests.java @@ -60,6 +60,14 @@ public class SubjectX500PrincipalExtractorTests { assertThat(principal).isEqualTo("luke"); } + // gh-19254 + @Test + void extractWhenMultipleCnsThenExtractsMostSpecificCn() throws Exception { + Object principal = this.extractor.extractPrincipal(X509TestUtils.buildTestCertificateWithMultipleCns()); + + assertThat(principal).isEqualTo("alice"); + } + @Test void extractWhenEmailDnEmbeddedInCnThenExtractsEmail() throws Exception { this.extractor.setExtractPrincipalNameFromEmail(true); diff --git a/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/X509TestUtils.java b/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/X509TestUtils.java index 66c392b77f..d870ac8fb9 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/X509TestUtils.java +++ b/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/X509TestUtils.java @@ -185,4 +185,38 @@ public final class X509TestUtils { return (X509Certificate) cf.generateCertificate(in); } + /** + * Builds an X.509 certificate whose subject contains more than one CN. The subject DN + * is: + * + *
+	 *  CN=alice, CN=bob, O=Example Corp, C=US
+	 * 
+ * + * where {@code CN=alice} is the most specific (left-most) RDN. + */ + public static X509Certificate buildTestCertificateWithMultipleCns() throws Exception { + String cert = "-----BEGIN CERTIFICATE-----\n" + + "MIIDJzCCAg+gAwIBAgIIclOz1VulWC8wDQYJKoZIhvcNAQEMBQAwQjELMAkGA1UE\n" + + "BhMCVVMxFTATBgNVBAoTDEV4YW1wbGUgQ29ycDEMMAoGA1UEAxMDYm9iMQ4wDAYD\n" + + "VQQDEwVhbGljZTAeFw0yNjA2MDExOTQzMTVaFw0zNjA1MjkxOTQzMTVaMEIxCzAJ\n" + + "BgNVBAYTAlVTMRUwEwYDVQQKEwxFeGFtcGxlIENvcnAxDDAKBgNVBAMTA2JvYjEO\n" + + "MAwGA1UEAxMFYWxpY2UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCX\n" + + "q8hZrTRHJEN7+D6yK65OKeCTVU+WccI6awz6g4T6O3aoC+1IiUljsEYn+xuDfx5L\n" + + "L/O1kejjmbYt+vzRmiILoJ9xKfW3ERcB4+gEar959Dkj6wmpsgOKRjmOvcOFOkEe\n" + + "gU1F7t04JHOou3DaAkHMNMQV+3jsWSh9Rry7XZBDkcT8XbbagoCgSIIef03Qtw31\n" + + "zOBwqmJmd6CQhFJva5cl8cCE2xZinOIiz/2j7VprZTjhkud2M4bRnK3T0WExyOCg\n" + + "jvjSf5ZoOKwC2Z9Q/Oyf8WKpren1+GfZhAKKmn7QZ2foHhdPPRtNjRGE6SqQyW2u\n" + + "8+1tOXl+aRCF19rR7gIpAgMBAAGjITAfMB0GA1UdDgQWBBRfLtnK5WKU0q3zxIrr\n" + + "y3GD+lYplzANBgkqhkiG9w0BAQwFAAOCAQEAe+/FHqErVPsF/sHrVHny8mIsn3ux\n" + + "qE9P24KNF0oIfmBrAqqge6hoVQ8PS+JialyqFf//osuDjiuYaBEKBw7GCoA6I8mr\n" + + "FA7wyFaGosfq7An5vxkJl7lap2u5oSVv3dCy13Bs0ziYmNlTkfHDLy9yh7jpH1wg\n" + + "TvylH9O4Vc7y9rzzpIjMCuQJ/wJ4MuJ2mSarYZsx3UQHIRfpKtR/9jbFMX1Rbv/A\n" + + "N0XD+NrtFjiikp71y3aCO1EHnGG7qPKCWh3PzaNoWFpyZKDBvud8ymW7RMiLPgv9\n" + + "eTa82KGgnCwtKCNzGkszIn7fza/6xnCuzvh4y9tYE5BWP2mcMRtRmDD07w==\n" + "-----END CERTIFICATE-----"; + ByteArrayInputStream in = new ByteArrayInputStream(cert.getBytes()); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(in); + } + }