Medium severity4.8NVD Advisory· Published Apr 21, 2026· Updated May 1, 2026
CVE-2026-22751
CVE-2026-22751
Description
Vulnerability in Spring Spring Security. Applications that explicitly configure One-Time Token login with JdbcOneTimeTokenService are vulnerable to a Time-of-check Time-of-use (TOCTOU) race condition. This issue affects Spring Security: from 6.4.0 through 6.4.15, from 6.5.0 through 6.5.9, from 7.0.0 through 7.0.4.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.springframework.security:spring-security-coreMaven | >= 6.5.0, < 6.5.10 | 6.5.10 |
org.springframework.security:spring-security-coreMaven | >= 7.0.3, < 7.0.5 | 7.0.5 |
org.springframework.security:spring-security-coreMaven | >= 6.4.0, <= 6.4.13 | — |
Affected products
1Patches
2163772775036Merge branch '6.5.x' into 7.0.x
8 files changed · +174 −19
core/src/main/java/org/springframework/security/authentication/dao/AbstractUserDetailsAuthenticationProvider.java+39 −4 modified@@ -97,6 +97,8 @@ public abstract class AbstractUserDetailsAuthenticationProvider private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks(); + private boolean alwaysPerformAdditionalChecksOnUser = true; + private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); private static final String AUTHORITY = FactorGrantedAuthority.PASSWORD_AUTHORITY; @@ -154,8 +156,7 @@ public Authentication authenticate(Authentication authentication) throws Authent Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try { - this.preAuthenticationChecks.check(user); - additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); + performPreCheck(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException ex) { if (!cacheWasUsed) { @@ -165,8 +166,7 @@ public Authentication authenticate(Authentication authentication) throws Authent // we're using latest data (i.e. not from the cache) cacheWasUsed = false; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); - this.preAuthenticationChecks.check(user); - additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); + performPreCheck(user, (UsernamePasswordAuthenticationToken) authentication); } this.postAuthenticationChecks.check(user); if (!cacheWasUsed) { @@ -179,6 +179,25 @@ public Authentication authenticate(Authentication authentication) throws Authent return createSuccessAuthentication(principalToReturn, authentication, user); } + private void performPreCheck(UserDetails user, UsernamePasswordAuthenticationToken authentication) { + try { + this.preAuthenticationChecks.check(user); + } + catch (AuthenticationException ex) { + if (!this.alwaysPerformAdditionalChecksOnUser) { + throw ex; + } + try { + additionalAuthenticationChecks(user, authentication); + } + catch (AuthenticationException ignored) { + // preserve the original failed check + } + throw ex; + } + additionalAuthenticationChecks(user, authentication); + } + private String determineUsername(Authentication authentication) { return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); } @@ -324,6 +343,22 @@ public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChe this.postAuthenticationChecks = postAuthenticationChecks; } + /** + * Set whether to always perform the additional checks on the user, even if the + * pre-authentication checks fail. This is useful to ensure that regardless of the + * state of the user account, authentication takes the same amount of time to + * complete. + * + * <p> + * For applications that rely on the additional checks running only once should set + * this value to {@code false} + * @param alwaysPerformAdditionalChecksOnUser + * @since 5.7.23 + */ + public void setAlwaysPerformAdditionalChecksOnUser(boolean alwaysPerformAdditionalChecksOnUser) { + this.alwaysPerformAdditionalChecksOnUser = alwaysPerformAdditionalChecksOnUser; + } + public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) { this.authoritiesMapper = authoritiesMapper; }
core/src/main/java/org/springframework/security/authentication/ott/JdbcOneTimeTokenService.java+5 −3 modified@@ -153,7 +153,9 @@ private void insertOneTimeToken(OneTimeToken oneTimeToken) { return null; } OneTimeToken token = tokens.get(0); - deleteOneTimeToken(token); + if (deleteOneTimeToken(token) == 0) { + return null; + } if (isExpired(token)) { return null; } @@ -171,11 +173,11 @@ private List<OneTimeToken> selectOneTimeToken(OneTimeTokenAuthenticationToken au return this.jdbcOperations.query(SELECT_ONE_TIME_TOKEN_SQL, pss, this.oneTimeTokenRowMapper); } - private void deleteOneTimeToken(OneTimeToken oneTimeToken) { + private int deleteOneTimeToken(OneTimeToken oneTimeToken) { List<SqlParameterValue> parameters = List .of(new SqlParameterValue(Types.VARCHAR, oneTimeToken.getTokenValue())); PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray()); - this.jdbcOperations.update(DELETE_ONE_TIME_TOKEN_SQL, pss); + return this.jdbcOperations.update(DELETE_ONE_TIME_TOKEN_SQL, pss); } private @Nullable ThreadPoolTaskScheduler createTaskScheduler(String cleanupCron) {
core/src/test/java/org/springframework/security/authentication/dao/DaoAuthenticationProviderTests.java+42 −6 modified@@ -21,6 +21,7 @@ import java.util.List; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; import org.springframework.cache.Cache; import org.springframework.dao.DataRetrievalFailureException; @@ -44,6 +45,7 @@ import org.springframework.security.core.userdetails.PasswordEncodedUser; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsChecker; import org.springframework.security.core.userdetails.UserDetailsPasswordService; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; @@ -64,6 +66,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -422,12 +425,10 @@ public void constructWhenPasswordEncoderProvidedThenSets() { assertThat(daoAuthenticationProvider.getPasswordEncoder()).isSameAs(NoOpPasswordEncoder.getInstance()); } - /** - * This is an explicit test for SEC-2056. It is intentionally ignored since this test - * is not deterministic and {@link #testUserNotFoundEncodesPassword()} ensures that - * SEC-2056 is fixed. - */ - public void IGNOREtestSec2056() { + // SEC-2056 + @Test + @EnabledIfSystemProperty(named = "spring.security.timing-tests", matches = "true") + public void testSec2056() { UsernamePasswordAuthenticationToken foundUser = UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala"); UsernamePasswordAuthenticationToken notFoundUser = UsernamePasswordAuthenticationToken @@ -460,6 +461,41 @@ public void IGNOREtestSec2056() { .isTrue(); } + // related to SEC-2056 + @Test + @EnabledIfSystemProperty(named = "spring.security.timing-tests", matches = "true") + public void testDisabledUserTiming() { + UsernamePasswordAuthenticationToken user = UsernamePasswordAuthenticationToken.unauthenticated("rod", "koala"); + PasswordEncoder encoder = new BCryptPasswordEncoder(); + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setPasswordEncoder(encoder); + MockUserDetailsServiceUserRod users = new MockUserDetailsServiceUserRod(); + users.password = encoder.encode((CharSequence) user.getCredentials()); + provider.setUserDetailsService(users); + int sampleSize = 100; + List<Long> enabledTimes = new ArrayList<>(sampleSize); + for (int i = 0; i < sampleSize; i++) { + long start = System.currentTimeMillis(); + provider.authenticate(user); + enabledTimes.add(System.currentTimeMillis() - start); + } + UserDetailsChecker preChecks = mock(UserDetailsChecker.class); + willThrow(new DisabledException("User is disabled")).given(preChecks).check(any(UserDetails.class)); + provider.setPreAuthenticationChecks(preChecks); + List<Long> disabledTimes = new ArrayList<>(sampleSize); + for (int i = 0; i < sampleSize; i++) { + long start = System.currentTimeMillis(); + assertThatExceptionOfType(DisabledException.class).isThrownBy(() -> provider.authenticate(user)); + disabledTimes.add(System.currentTimeMillis() - start); + } + double enabledAvg = avg(enabledTimes); + double disabledAvg = avg(disabledTimes); + assertThat(Math.abs(disabledAvg - enabledAvg) <= 3) + .withFailMessage("Disabled user average " + disabledAvg + " should be within 3ms of enabled user average " + + enabledAvg) + .isTrue(); + } + private double avg(List<Long> counts) { return counts.stream().mapToLong(Long::longValue).average().orElse(0); }
core/src/test/java/org/springframework/security/authentication/ott/JdbcOneTimeTokenServiceTests.java+26 −0 modified@@ -21,19 +21,24 @@ import java.time.Instant; import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; +import java.util.List; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.PreparedStatementSetter; +import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -145,6 +150,27 @@ void consumeWhenTokenDoesNotExistsThenReturnNull() { assertThat(consumedOneTimeToken).isNull(); } + @Test + void consumeWhenTokenIsDeletedConcurrentlyThenReturnNull() throws Exception { + // Simulates a concurrent consume: SELECT finds the token but DELETE affects + // 0 rows because another caller already consumed it. + JdbcOperations jdbcOperations = mock(JdbcOperations.class); + Instant notExpired = Instant.now().plus(5, ChronoUnit.MINUTES); + OneTimeToken token = new DefaultOneTimeToken(TOKEN_VALUE, USERNAME, notExpired); + given(jdbcOperations.query(any(String.class), any(PreparedStatementSetter.class), + ArgumentMatchers.<RowMapper<OneTimeToken>>any())) + .willReturn(List.of(token)); + given(jdbcOperations.update(any(String.class), any(PreparedStatementSetter.class))).willReturn(0); + JdbcOneTimeTokenService service = new JdbcOneTimeTokenService(jdbcOperations); + try { + OneTimeToken consumed = service.consume(new OneTimeTokenAuthenticationToken(TOKEN_VALUE)); + assertThat(consumed).isNull(); + } + finally { + service.destroy(); + } + } + @Test void consumeWhenTokenIsExpiredThenReturnNull() { GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest(USERNAME);
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoder.java+13 −2 modified@@ -232,7 +232,8 @@ public static JwkSetUriJwtDecoderBuilder withIssuerLocation(String issuer) { .getConfigurationForIssuerLocation(issuer, rest); JwtDecoderProviderConfigurationUtils.validateIssuer(configuration, issuer); return configuration.get("jwks_uri").toString(); - }, JwtDecoderProviderConfigurationUtils::getJWSAlgorithms); + }, JwtDecoderProviderConfigurationUtils::getJWSAlgorithms) + .validator(JwtValidators.createDefaultWithIssuer(issuer)); } /** @@ -301,6 +302,8 @@ public static final class JwkSetUriJwtDecoderBuilder { private Consumer<ConfigurableJWTProcessor<SecurityContext>> jwtProcessorCustomizer; + private OAuth2TokenValidator<Jwt> validator = JwtValidators.createDefault(); + private JwkSetUriJwtDecoderBuilder(String jwkSetUri) { Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty"); this.jwkSetUri = (rest) -> jwkSetUri; @@ -441,6 +444,12 @@ public JwkSetUriJwtDecoderBuilder jwtProcessorCustomizer( return this; } + JwkSetUriJwtDecoderBuilder validator(OAuth2TokenValidator<Jwt> validator) { + Assert.notNull(validator, "validator cannot be null"); + this.validator = validator; + return this; + } + JWSKeySelector<SecurityContext> jwsKeySelector(JWKSource<SecurityContext> jwkSource) { if (this.signatureAlgorithms.isEmpty()) { return new JWSVerificationKeySelector<>(this.defaultAlgorithms.apply(jwkSource), jwkSource); @@ -479,7 +488,9 @@ JWTProcessor<SecurityContext> processor() { * @return the configured {@link NimbusJwtDecoder} */ public NimbusJwtDecoder build() { - return new NimbusJwtDecoder(processor()); + NimbusJwtDecoder decoder = new NimbusJwtDecoder(processor()); + decoder.setJwtValidator(this.validator); + return decoder; } private static final class SpringJWKSource<C extends SecurityContext> implements JWKSetSource<C> {
oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoder.java+12 −2 modified@@ -241,7 +241,8 @@ public static JwkSetUriReactiveJwtDecoderBuilder withIssuerLocation(String issue } return Mono.just(configuration.get("jwks_uri").toString()); }), - ReactiveJwtDecoderProviderConfigurationUtils::getJWSAlgorithms); + ReactiveJwtDecoderProviderConfigurationUtils::getJWSAlgorithms) + .validator(JwtValidators.createDefaultWithIssuer(issuer)); } /** @@ -332,6 +333,8 @@ public static final class JwkSetUriReactiveJwtDecoderBuilder { private BiFunction<ReactiveRemoteJWKSource, ConfigurableJWTProcessor<JWKSecurityContext>, Mono<ConfigurableJWTProcessor<JWKSecurityContext>>> jwtProcessorCustomizer; + private OAuth2TokenValidator<Jwt> validator = JwtValidators.createDefault(); + private JwkSetUriReactiveJwtDecoderBuilder(String jwkSetUri) { Assert.hasText(jwkSetUri, "jwkSetUri cannot be empty"); this.jwkSetUri = (web) -> Mono.just(jwkSetUri); @@ -456,6 +459,11 @@ public JwkSetUriReactiveJwtDecoderBuilder jwtProcessorCustomizer( return this; } + JwkSetUriReactiveJwtDecoderBuilder validator(OAuth2TokenValidator<Jwt> validator) { + this.validator = validator; + return this; + } + JwkSetUriReactiveJwtDecoderBuilder jwtProcessorCustomizer( BiFunction<ReactiveRemoteJWKSource, ConfigurableJWTProcessor<JWKSecurityContext>, Mono<ConfigurableJWTProcessor<JWKSecurityContext>>> jwtProcessorCustomizer) { Assert.notNull(jwtProcessorCustomizer, "jwtProcessorCustomizer cannot be null"); @@ -468,7 +476,9 @@ JwkSetUriReactiveJwtDecoderBuilder jwtProcessorCustomizer( * @return the configured {@link NimbusReactiveJwtDecoder} */ public NimbusReactiveJwtDecoder build() { - return new NimbusReactiveJwtDecoder(processor()); + NimbusReactiveJwtDecoder decoder = new NimbusReactiveJwtDecoder(processor()); + decoder.setJwtValidator(this.validator); + return decoder; } Mono<JWSKeySelector<JWKSecurityContext>> jwsKeySelector(ReactiveRemoteJWKSource source) {
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusJwtDecoderTests.java+16 −1 modified@@ -332,7 +332,10 @@ public void decodeWhenIssuerLocationThenOk() { .willReturn(new ResponseEntity<>(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks"), HttpStatus.OK)); given(restOperations.exchange(any(RequestEntity.class), eq(String.class))) .willReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK)); - JwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(restOperations).build(); + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer) + .restOperations(restOperations) + .build(); + jwtDecoder.setJwtValidator(JwtValidators.createDefault()); Jwt jwt = jwtDecoder.decode(SIGNED_JWT); assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull(); } @@ -350,6 +353,18 @@ public void decodeWhenDiscoverJwsAlgorithmsThenOk() { assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull(); } + @Test + public void decodeWhenIssuerLocationThenRejectsMismatchingIssuers() { + String issuer = "https://example.org/wrong-issuer"; + RestOperations restOperations = mock(RestOperations.class); + given(restOperations.exchange(any(RequestEntity.class), any(ParameterizedTypeReference.class))) + .willReturn(new ResponseEntity<>(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks"), HttpStatus.OK)); + given(restOperations.exchange(any(RequestEntity.class), eq(String.class))) + .willReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK)); + JwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(issuer).restOperations(restOperations).build(); + assertThatExceptionOfType(JwtValidationException.class).isThrownBy(() -> jwtDecoder.decode(SIGNED_JWT)); + } + @Test public void withJwkSetUriWhenNullOrEmptyThenThrowsException() { // @formatter:off
oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/NimbusReactiveJwtDecoderTests.java+21 −1 modified@@ -617,13 +617,33 @@ public void decodeWhenIssuerLocationThenOk() { given(responseSpec.bodyToMono(any(ParameterizedTypeReference.class))) .willReturn(Mono.just(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks"))); given(spec.retrieve()).willReturn(responseSpec); - ReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer) + NimbusReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer) .webClient(webClient) .build(); + jwtDecoder.setJwtValidator(JwtValidators.createDefault()); Jwt jwt = jwtDecoder.decode(this.messageReadToken).block(); assertThat(jwt.hasClaim(JwtClaimNames.EXP)).isNotNull(); } + @Test + public void decodeWhenIssuerLocationThenRejectsMismatchingIssuers() { + String issuer = "https://example.org/wrong-issuer"; + WebClient real = WebClient.builder().build(); + WebClient.RequestHeadersUriSpec spec = spy(real.get()); + WebClient webClient = spy(WebClient.class); + given(webClient.get()).willReturn(spec); + WebClient.ResponseSpec responseSpec = mock(WebClient.ResponseSpec.class); + given(responseSpec.bodyToMono(String.class)).willReturn(Mono.just(this.jwkSet)); + given(responseSpec.bodyToMono(any(ParameterizedTypeReference.class))) + .willReturn(Mono.just(Map.of("issuer", issuer, "jwks_uri", issuer + "/jwks"))); + given(spec.retrieve()).willReturn(responseSpec); + ReactiveJwtDecoder jwtDecoder = NimbusReactiveJwtDecoder.withIssuerLocation(issuer) + .webClient(webClient) + .build(); + assertThatExceptionOfType(JwtValidationException.class) + .isThrownBy(() -> jwtDecoder.decode(this.messageReadToken).block()); + } + @Test public void jwsKeySelectorWhenNoAlgorithmThenReturnsRS256Selector() { ReactiveRemoteJWKSource jwkSource = mock(ReactiveRemoteJWKSource.class);
4187af38b251Verify token deletion in JdbcOneTimeTokenService
2 files changed · +31 −3
core/src/main/java/org/springframework/security/authentication/ott/JdbcOneTimeTokenService.java+5 −3 modified@@ -152,7 +152,9 @@ public OneTimeToken consume(OneTimeTokenAuthenticationToken authenticationToken) return null; } OneTimeToken token = tokens.get(0); - deleteOneTimeToken(token); + if (deleteOneTimeToken(token) == 0) { + return null; + } if (isExpired(token)) { return null; } @@ -170,11 +172,11 @@ private List<OneTimeToken> selectOneTimeToken(OneTimeTokenAuthenticationToken au return this.jdbcOperations.query(SELECT_ONE_TIME_TOKEN_SQL, pss, this.oneTimeTokenRowMapper); } - private void deleteOneTimeToken(OneTimeToken oneTimeToken) { + private int deleteOneTimeToken(OneTimeToken oneTimeToken) { List<SqlParameterValue> parameters = List .of(new SqlParameterValue(Types.VARCHAR, oneTimeToken.getTokenValue())); PreparedStatementSetter pss = new ArgumentPreparedStatementSetter(parameters.toArray()); - this.jdbcOperations.update(DELETE_ONE_TIME_TOKEN_SQL, pss); + return this.jdbcOperations.update(DELETE_ONE_TIME_TOKEN_SQL, pss); } private ThreadPoolTaskScheduler createTaskScheduler(String cleanupCron) {
core/src/test/java/org/springframework/security/authentication/ott/JdbcOneTimeTokenServiceTests.java+26 −0 modified@@ -21,19 +21,24 @@ import java.time.Instant; import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; +import java.util.List; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.PreparedStatementSetter; +import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -145,6 +150,27 @@ void consumeWhenTokenDoesNotExistsThenReturnNull() { assertThat(consumedOneTimeToken).isNull(); } + @Test + void consumeWhenTokenIsDeletedConcurrentlyThenReturnNull() throws Exception { + // Simulates a concurrent consume: SELECT finds the token but DELETE affects + // 0 rows because another caller already consumed it. + JdbcOperations jdbcOperations = mock(JdbcOperations.class); + Instant notExpired = Instant.now().plus(5, ChronoUnit.MINUTES); + OneTimeToken token = new DefaultOneTimeToken(TOKEN_VALUE, USERNAME, notExpired); + given(jdbcOperations.query(any(String.class), any(PreparedStatementSetter.class), + ArgumentMatchers.<RowMapper<OneTimeToken>>any())) + .willReturn(List.of(token)); + given(jdbcOperations.update(any(String.class), any(PreparedStatementSetter.class))).willReturn(0); + JdbcOneTimeTokenService service = new JdbcOneTimeTokenService(jdbcOperations); + try { + OneTimeToken consumed = service.consume(new OneTimeTokenAuthenticationToken(TOKEN_VALUE)); + assertThat(consumed).isNull(); + } + finally { + service.destroy(); + } + } + @Test void consumeWhenTokenIsExpiredThenReturnNull() { GenerateOneTimeTokenRequest request = new GenerateOneTimeTokenRequest(USERNAME);
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-x2wq-9x2f-fhj7ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-22751ghsaADVISORY
- spring.io/security/cve-2026-22751nvdVendor AdvisoryWEB
- github.com/spring-projects/spring-security/commit/163772775036c4146815a5266874278c6f45f047ghsaWEB
- github.com/spring-projects/spring-security/commit/4187af38b251fc97fdf9949f7869618111e6e261ghsaWEB
News mentions
0No linked articles in our index yet.