Skip to content

Commit 32d6661

Browse files
GHi-464: Updated dependencies for netty + CVE exclusion for false positive
Updated dependencies for netty, and excluded CVE-2026-22747 for spring security web 6.5.10 (+ tests to show the CVE really is a false positive).
1 parent 463e867 commit 32d6661

5 files changed

Lines changed: 227 additions & 1 deletion

File tree

backend/core/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ dependencies {
141141
testImplementation "org.junit.jupiter:junit-jupiter-engine"
142142
testImplementation "org.apache.groovy:groovy-all:${groovyVersion}"
143143

144+
// BouncyCastle for X.509 certificate generation in vulnerability tests
145+
testImplementation "org.bouncycastle:bcprov-jdk18on:1.84"
146+
testImplementation "org.bouncycastle:bcpkix-jdk18on:1.84"
147+
144148
configurations.all {
145149
exclude group: 'org.graalvm.truffle', module: 'truffle-enterprise'
146150
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Copyright 2015-2024 Ritense BV, the Netherlands.
3+
*
4+
* Licensed under EUPL, Version 1.2 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" basis,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.ritense.valtimo.security.x509
18+
19+
import org.assertj.core.api.Assertions.assertThat
20+
import org.junit.jupiter.api.Assertions.fail
21+
import org.junit.jupiter.api.DisplayName
22+
import org.junit.jupiter.api.Test
23+
import org.junit.jupiter.params.ParameterizedTest
24+
import org.junit.jupiter.params.provider.Arguments
25+
import org.junit.jupiter.params.provider.MethodSource
26+
import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor
27+
import java.util.stream.Stream
28+
29+
class SubjectDnX509PrincipalExtractorVulnerabilityTest {
30+
31+
private val extractor = SubjectDnX509PrincipalExtractor()
32+
33+
@Test
34+
@DisplayName("Legitimate certificate extracts correct CN")
35+
fun `legitimate certificate extracts correct CN`() {
36+
val cert = X509TestCertificateGenerator.createCertificate("CN=legitimate-user")
37+
38+
val principal = extractor.extractPrincipal(cert) as String
39+
40+
assertThat(principal).isEqualTo("legitimate-user")
41+
}
42+
43+
@Test
44+
@DisplayName("Certificate with multiple RDNs extracts first CN")
45+
fun `certificate with multiple RDNs extracts first CN`() {
46+
val cert = X509TestCertificateGenerator.createCertificate(
47+
"CN=actual-user,OU=Department,O=Organization,C=NL"
48+
)
49+
50+
val principal = extractor.extractPrincipal(cert) as String
51+
52+
assertThat(principal).isEqualTo("actual-user")
53+
}
54+
55+
@Test
56+
@DisplayName("Multi-valued RDN extracts CN from first RDN (expected RFC 2253 behavior)")
57+
fun `multi-valued RDN extracts CN from first RDN`() {
58+
val cert = X509TestCertificateGenerator.createCertificate(
59+
"CN=admin+OU=fake,CN=real-user"
60+
)
61+
62+
val principal = extractor.extractPrincipal(cert) as String
63+
64+
assertThat(principal).isEqualTo("admin")
65+
}
66+
67+
@ParameterizedTest(name = "[{index}] DN: {0}")
68+
@MethodSource("malformedDNsWithExpectedResults")
69+
@DisplayName("Malformed DN should not allow impersonation")
70+
fun `malformed DN should not allow impersonation`(
71+
subjectDN: String,
72+
forbiddenPrincipal: String,
73+
description: String
74+
) {
75+
val cert = try {
76+
X509TestCertificateGenerator.createCertificate(subjectDN)
77+
} catch (e: Exception) {
78+
return
79+
}
80+
81+
val extractedPrincipal = extractor.extractPrincipal(cert) as String
82+
83+
if (forbiddenPrincipal == extractedPrincipal) {
84+
fail<Unit>(
85+
"""
86+
VULNERABILITY DETECTED! $description
87+
Subject DN: $subjectDN
88+
Extracted principal: $extractedPrincipal
89+
This should NOT have extracted '$forbiddenPrincipal' - potential impersonation attack!
90+
""".trimIndent()
91+
)
92+
}
93+
}
94+
95+
companion object {
96+
@JvmStatic
97+
fun malformedDNsWithExpectedResults(): Stream<Arguments> = Stream.of(
98+
Arguments.of(
99+
"OU=CN\\=admin,CN=attacker",
100+
"admin",
101+
"CN embedded in OU value with escaped equals"
102+
),
103+
Arguments.of(
104+
"CN=admin\\,CN=victim",
105+
"admin",
106+
"Escaped comma attempting to create fake second CN"
107+
),
108+
Arguments.of(
109+
"CN=admin\\\\,CN=real-user",
110+
"admin",
111+
"Double backslash before comma"
112+
),
113+
Arguments.of(
114+
"O=CN\\=admin,CN=legitimate",
115+
"admin",
116+
"CN pattern embedded in Organization"
117+
),
118+
Arguments.of(
119+
"CN=first,CN=second,O=Org",
120+
"second",
121+
"Multiple CN attributes - should pick first, not second"
122+
),
123+
Arguments.of(
124+
"CN=\\ admin\\ ,O=Org",
125+
"admin",
126+
"CN with leading/trailing spaces should include spaces"
127+
),
128+
Arguments.of(
129+
"CN=,CN=real-user",
130+
"real-user",
131+
"Empty CN followed by populated CN"
132+
),
133+
Arguments.of(
134+
"CN=#61646D696E,O=Org",
135+
"admin",
136+
"Hex-encoded 'admin' in CN - verify proper decoding"
137+
),
138+
Arguments.of(
139+
"2.5.4.3=#61646D696E,CN=real-user",
140+
"admin",
141+
"OID 2.5.4.3 (CN) with hex value before string CN"
142+
)
143+
)
144+
}
145+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2015-2024 Ritense BV, the Netherlands.
3+
*
4+
* Licensed under EUPL, Version 1.2 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" basis,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.ritense.valtimo.security.x509
18+
19+
import org.bouncycastle.asn1.x500.X500Name
20+
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
21+
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
22+
import org.bouncycastle.jce.provider.BouncyCastleProvider
23+
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
24+
import java.math.BigInteger
25+
import java.security.KeyPairGenerator
26+
import java.security.Security
27+
import java.security.cert.X509Certificate
28+
import java.time.Instant
29+
import java.time.temporal.ChronoUnit
30+
import java.util.Date
31+
32+
object X509TestCertificateGenerator {
33+
34+
init {
35+
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
36+
Security.addProvider(BouncyCastleProvider())
37+
}
38+
}
39+
40+
private val keyPair = KeyPairGenerator.getInstance("RSA").apply { initialize(2048) }.generateKeyPair()
41+
42+
fun createCertificate(subjectDN: String): X509Certificate {
43+
val subject = X500Name(subjectDN)
44+
val now = Instant.now()
45+
val notBefore = Date.from(now)
46+
val notAfter = Date.from(now.plus(365, ChronoUnit.DAYS))
47+
48+
val certBuilder = JcaX509v3CertificateBuilder(
49+
subject,
50+
BigInteger.valueOf(System.currentTimeMillis()),
51+
notBefore,
52+
notAfter,
53+
subject,
54+
keyPair.public
55+
)
56+
57+
val signer = JcaContentSignerBuilder("SHA256WithRSAEncryption")
58+
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
59+
.build(keyPair.private)
60+
61+
val certHolder = certBuilder.build(signer)
62+
63+
return JcaX509CertificateConverter()
64+
.setProvider(BouncyCastleProvider.PROVIDER_NAME)
65+
.getCertificate(certHolder)
66+
}
67+
}

backend/gradle/cve-report/cve-exclusions.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,14 @@
4646
<packageUrl regex="true">^pkg:maven/org\.graalvm\.shadowed/icu4j@25\.0\.1$</packageUrl>
4747
<cve>CVE-2025-5222</cve>
4848
</suppress>
49+
<suppress>
50+
<notes>CVE-2026-22747: SubjectX500PrincipalExtractor vulnerability in Spring Security.
51+
This CVE affects versions 6.5.0-6.5.9 and 7.0.0-7.0.4. Version 6.5.10 is the FIXED version.
52+
Verified via SubjectDnX509PrincipalExtractorVulnerabilityTest which tests various malformed
53+
X.509 certificate DN patterns - all pass on 6.5.10, confirming the fix is in place.
54+
Additionally, Valtimo does not use X.509 client certificate authentication.
55+
</notes>
56+
<packageUrl regex="true">^pkg:maven/org\.springframework\.security/spring\-security\-web@6\.5\.10$</packageUrl>
57+
<cve>CVE-2026-22747</cve>
58+
</suppress>
4959
</suppressions>

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ mandrillLutungClientVersion=0.0.8
7474
apacheHttpclientVersion=4.5.14
7575
plexusUtilsVersion=3.6.1
7676
log4jVersion=2.25.4
77-
nettyVersion=4.1.134.Final
77+
nettyVersion=4.1.135.Final
7878
tomcatVersion=10.1.55
7979
classgraphVersion=4.8.184
8080
guavaVersion=33.5.0-jre

0 commit comments

Comments
 (0)