VYPR
Critical severityNVD Advisory· Published Mar 27, 2023· Updated Feb 19, 2025

CVE-2023-20860

CVE-2023-20860

Description

Spring Framework running version 6.0.0 - 6.0.6 or 5.3.0 - 5.3.25 using "**" as a pattern in Spring Security configuration with the mvcRequestMatcher creates a mismatch in pattern matching between Spring Security and Spring MVC, and the potential for a security bypass.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A double wildcard pattern in Spring Security mvcRequestMatcher mismatches Spring MVC, allowing unauthenticated access to protected resources.

Vulnerability

Description

CVE-2023-20860 is a security bypass vulnerability in the Spring Framework affecting versions 6.0.0 through 6.0.6 and 5.3.0 through 5.3.25 [2]. The root cause lies in a mismatch between how Spring Security's mvcRequestMatcher and Spring MVC's HandlerMapping interpret the ** (double wildcard) pattern when used without a path prefix in security configurations [1][4]. This can lead to improper authorization decisions.

Attack

Vector and Exploitation

An attacker can exploit this vulnerability by crafting a request that targets a protected endpoint where the security configuration uses the ** pattern with an mvcRequestMatcher [4]. The mismatch causes Spring Security to incorrectly evaluate the pattern, potentially allowing the request to bypass the intended access controls. No authentication or elevated privileges are required from the attacker, as the bypass occurs during the authorization check [2].

Impact

Successful exploitation allows an unauthenticated attacker to gain unauthorized access to resources that should be protected by Spring Security. This can result in exposure of sensitive data, modification of data, or other unintended operations depending on the protected endpoint [2].

Mitigation

The vulnerability has been patched in Spring Framework 6.0.7 and 5.3.26 [4]. Users are strongly advised to upgrade to these or later versions. No workarounds were provided by the vendor. The fix involves correcting the pattern matching logic in HandlerMappingIntrospector to ensure consistent behavior between Spring Security and Spring MVC [1].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.springframework:springMaven
>= 6.0.0, < 6.0.76.0.7
org.springframework:springMaven
>= 5.3.0, < 5.3.265.3.26
org.springframework:spring-webmvcMaven
>= 6.0.0, < 6.0.76.0.7
org.springframework:spring-webmvcMaven
>= 5.3.0, < 5.3.265.3.26

Affected products

3

Patches

1
202fa5cdb3a3

Polishing and minor refactoring in HandlerMappingIntrospector

2 files changed · +81 83
  • spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java+75 78 modified
    @@ -82,7 +82,7 @@ public class HandlerMappingIntrospector
     	@Nullable
     	private List<HandlerMapping> handlerMappings;
     
    -	private Map<HandlerMapping, PathPatternMatchableHandlerMapping> pathPatternHandlerMappings = Collections.emptyMap();
    +	private Map<HandlerMapping, PathPatternMatchableHandlerMapping> pathPatternMappings = Collections.emptyMap();
     
     
     	@Override
    @@ -95,10 +95,55 @@ public void afterPropertiesSet() {
     		if (this.handlerMappings == null) {
     			Assert.notNull(this.applicationContext, "No ApplicationContext");
     			this.handlerMappings = initHandlerMappings(this.applicationContext);
    -			this.pathPatternHandlerMappings = initPathPatternMatchableHandlerMappings(this.handlerMappings);
    +
    +			this.pathPatternMappings = this.handlerMappings.stream()
    +					.filter(m -> m instanceof MatchableHandlerMapping hm && hm.getPatternParser() != null)
    +					.map(mapping -> (MatchableHandlerMapping) mapping)
    +					.collect(Collectors.toMap(mapping -> mapping, PathPatternMatchableHandlerMapping::new));
     		}
     	}
     
    +	private static List<HandlerMapping> initHandlerMappings(ApplicationContext context) {
    +
    +		Map<String, HandlerMapping> beans =
    +				BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
    +
    +		if (!beans.isEmpty()) {
    +			List<HandlerMapping> mappings = new ArrayList<>(beans.values());
    +			AnnotationAwareOrderComparator.sort(mappings);
    +			return Collections.unmodifiableList(mappings);
    +		}
    +
    +		return Collections.unmodifiableList(initFallback(context));
    +	}
    +
    +	private static List<HandlerMapping> initFallback(ApplicationContext applicationContext) {
    +		Properties properties;
    +		try {
    +			Resource resource = new ClassPathResource("DispatcherServlet.properties", DispatcherServlet.class);
    +			properties = PropertiesLoaderUtils.loadProperties(resource);
    +		}
    +		catch (IOException ex) {
    +			throw new IllegalStateException("Could not load DispatcherServlet.properties: " + ex.getMessage());
    +		}
    +
    +		String value = properties.getProperty(HandlerMapping.class.getName());
    +		String[] names = StringUtils.commaDelimitedListToStringArray(value);
    +		List<HandlerMapping> result = new ArrayList<>(names.length);
    +		for (String name : names) {
    +			try {
    +				Class<?> clazz = ClassUtils.forName(name, DispatcherServlet.class.getClassLoader());
    +				Object mapping = applicationContext.getAutowireCapableBeanFactory().createBean(clazz);
    +				result.add((HandlerMapping) mapping);
    +			}
    +			catch (ClassNotFoundException ex) {
    +				throw new IllegalStateException("Could not find default HandlerMapping [" + name + "]");
    +			}
    +		}
    +		return result;
    +	}
    +
    +
     	/**
     	 * Return the configured or detected {@code HandlerMapping}s.
     	 */
    @@ -109,27 +154,27 @@ public List<HandlerMapping> getHandlerMappings() {
     
     	/**
     	 * Find the {@link HandlerMapping} that would handle the given request and
    -	 * return it as a {@link MatchableHandlerMapping} that can be used to test
    -	 * request-matching criteria.
    -	 * <p>If the matching HandlerMapping is not an instance of
    -	 * {@link MatchableHandlerMapping}, an IllegalStateException is raised.
    +	 * return a {@link MatchableHandlerMapping} to use for path matching.
     	 * @param request the current request
    -	 * @return the resolved matcher, or {@code null}
    +	 * @return the resolved {@code MatchableHandlerMapping}, or {@code null}
    +	 * @throws IllegalStateException if the matching HandlerMapping is not an
    +	 * instance of {@link MatchableHandlerMapping}
     	 * @throws Exception if any of the HandlerMapping's raise an exception
     	 */
     	@Nullable
     	public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception {
     		HttpServletRequest wrappedRequest = new AttributesPreservingRequest(request);
    -		return doWithMatchingMapping(wrappedRequest, false, (matchedMapping, executionChain) -> {
    -			if (matchedMapping instanceof MatchableHandlerMapping matchableHandlerMapping) {
    -				PathPatternMatchableHandlerMapping mapping = this.pathPatternHandlerMappings.get(matchedMapping);
    -				if (mapping != null) {
    +
    +		return doWithHandlerMapping(wrappedRequest, false, (mapping, executionChain) -> {
    +			if (mapping instanceof MatchableHandlerMapping) {
    +				PathPatternMatchableHandlerMapping pathPatternMapping = this.pathPatternMappings.get(mapping);
    +				if (pathPatternMapping != null) {
     					RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(wrappedRequest);
    -					return new PathSettingHandlerMapping(mapping, requestPath);
    +					return new LookupPathMatchableHandlerMapping(pathPatternMapping, requestPath);
     				}
     				else {
     					String lookupPath = (String) wrappedRequest.getAttribute(UrlPathHelper.PATH_ATTRIBUTE);
    -					return new PathSettingHandlerMapping(matchableHandlerMapping, lookupPath);
    +					return new LookupPathMatchableHandlerMapping((MatchableHandlerMapping) mapping, lookupPath);
     				}
     			}
     			throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping");
    @@ -140,7 +185,7 @@ public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest req
     	@Nullable
     	public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
     		AttributesPreservingRequest wrappedRequest = new AttributesPreservingRequest(request);
    -		return doWithMatchingMappingIgnoringException(wrappedRequest, (handlerMapping, executionChain) -> {
    +		return doWithHandlerMappingIgnoringException(wrappedRequest, (handlerMapping, executionChain) -> {
     			for (HandlerInterceptor interceptor : executionChain.getInterceptorList()) {
     				if (interceptor instanceof CorsConfigurationSource ccs) {
     					return ccs.getCorsConfiguration(wrappedRequest);
    @@ -154,15 +199,15 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
     	}
     
     	@Nullable
    -	private <T> T doWithMatchingMapping(
    +	private <T> T doWithHandlerMapping(
     			HttpServletRequest request, boolean ignoreException,
    -			BiFunction<HandlerMapping, HandlerExecutionChain, T> matchHandler) throws Exception {
    +			BiFunction<HandlerMapping, HandlerExecutionChain, T> extractor) throws Exception {
     
    -		Assert.state(this.handlerMappings != null, "Handler mappings not initialized");
    +		Assert.state(this.handlerMappings != null, "HandlerMapping's not initialized");
     
    -		boolean parseRequestPath = !this.pathPatternHandlerMappings.isEmpty();
    +		boolean parsePath = !this.pathPatternMappings.isEmpty();
     		RequestPath previousPath = null;
    -		if (parseRequestPath) {
    +		if (parsePath) {
     			previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE);
     			ServletRequestPathUtils.parseAndCache(request);
     		}
    @@ -180,79 +225,30 @@ private <T> T doWithMatchingMapping(
     				if (chain == null) {
     					continue;
     				}
    -				return matchHandler.apply(handlerMapping, chain);
    +				return extractor.apply(handlerMapping, chain);
     			}
     		}
     		finally {
    -			if (parseRequestPath) {
    +			if (parsePath) {
     				ServletRequestPathUtils.setParsedRequestPath(previousPath, request);
     			}
     		}
     		return null;
     	}
     
     	@Nullable
    -	private <T> T doWithMatchingMappingIgnoringException(
    +	private <T> T doWithHandlerMappingIgnoringException(
     			HttpServletRequest request, BiFunction<HandlerMapping, HandlerExecutionChain, T> matchHandler) {
     
     		try {
    -			return doWithMatchingMapping(request, true, matchHandler);
    +			return doWithHandlerMapping(request, true, matchHandler);
     		}
     		catch (Exception ex) {
     			throw new IllegalStateException("HandlerMapping exception not suppressed", ex);
     		}
     	}
     
     
    -	private static List<HandlerMapping> initHandlerMappings(ApplicationContext applicationContext) {
    -		Map<String, HandlerMapping> beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(
    -				applicationContext, HandlerMapping.class, true, false);
    -		if (!beans.isEmpty()) {
    -			List<HandlerMapping> mappings = new ArrayList<>(beans.values());
    -			AnnotationAwareOrderComparator.sort(mappings);
    -			return Collections.unmodifiableList(mappings);
    -		}
    -		return Collections.unmodifiableList(initFallback(applicationContext));
    -	}
    -
    -	private static List<HandlerMapping> initFallback(ApplicationContext applicationContext) {
    -		Properties props;
    -		String path = "DispatcherServlet.properties";
    -		try {
    -			Resource resource = new ClassPathResource(path, DispatcherServlet.class);
    -			props = PropertiesLoaderUtils.loadProperties(resource);
    -		}
    -		catch (IOException ex) {
    -			throw new IllegalStateException("Could not load '" + path + "': " + ex.getMessage());
    -		}
    -
    -		String value = props.getProperty(HandlerMapping.class.getName());
    -		String[] names = StringUtils.commaDelimitedListToStringArray(value);
    -		List<HandlerMapping> result = new ArrayList<>(names.length);
    -		for (String name : names) {
    -			try {
    -				Class<?> clazz = ClassUtils.forName(name, DispatcherServlet.class.getClassLoader());
    -				Object mapping = applicationContext.getAutowireCapableBeanFactory().createBean(clazz);
    -				result.add((HandlerMapping) mapping);
    -			}
    -			catch (ClassNotFoundException ex) {
    -				throw new IllegalStateException("Could not find default HandlerMapping [" + name + "]");
    -			}
    -		}
    -		return result;
    -	}
    -
    -	private static Map<HandlerMapping, PathPatternMatchableHandlerMapping> initPathPatternMatchableHandlerMappings(
    -			List<HandlerMapping> mappings) {
    -
    -		return mappings.stream()
    -				.filter(MatchableHandlerMapping.class::isInstance)
    -				.map(MatchableHandlerMapping.class::cast)
    -				.filter(mapping -> mapping.getPatternParser() != null)
    -				.collect(Collectors.toMap(mapping -> mapping, PathPatternMatchableHandlerMapping::new));
    -	}
    -
    -
     	/**
     	 * Request wrapper that buffers request attributes in order protect the
     	 * underlying request from attribute changes.
    @@ -298,26 +294,27 @@ public void removeAttribute(String name) {
     	}
     
     
    -	private static class PathSettingHandlerMapping implements MatchableHandlerMapping {
    +	private static class LookupPathMatchableHandlerMapping implements MatchableHandlerMapping {
     
     		private final MatchableHandlerMapping delegate;
     
    -		private final Object path;
    +		private final Object lookupPath;
     
     		private final String pathAttributeName;
     
    -		PathSettingHandlerMapping(MatchableHandlerMapping delegate, Object path) {
    +		LookupPathMatchableHandlerMapping(MatchableHandlerMapping delegate, Object lookupPath) {
     			this.delegate = delegate;
    -			this.path = path;
    -			this.pathAttributeName = (path instanceof RequestPath ?
    +			this.lookupPath = lookupPath;
    +			this.pathAttributeName = (lookupPath instanceof RequestPath ?
     					ServletRequestPathUtils.PATH_ATTRIBUTE : UrlPathHelper.PATH_ATTRIBUTE);
     		}
     
     		@Nullable
     		@Override
     		public RequestMatchResult match(HttpServletRequest request, String pattern) {
    +			pattern = (StringUtils.hasLength(pattern) && !pattern.startsWith("/") ? "/" + pattern : pattern);
     			Object previousPath = request.getAttribute(this.pathAttributeName);
    -			request.setAttribute(this.pathAttributeName, this.path);
    +			request.setAttribute(this.pathAttributeName, this.lookupPath);
     			try {
     				return this.delegate.match(request, pattern);
     			}
    
  • spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java+6 5 modified
    @@ -1,5 +1,5 @@
     /*
    - * Copyright 2002-2022 the original author or authors.
    + * Copyright 2002-2023 the original author or authors.
      *
      * Licensed under the Apache License, Version 2.0 (the "License");
      * you may not use this file except in compliance with the License.
    @@ -30,8 +30,9 @@
     import org.springframework.web.util.pattern.PathPatternParser;
     
     /**
    - * Wraps {@link MatchableHandlerMapping}s configured with a {@link PathPatternParser}
    - * in order to parse patterns lazily and cache them for re-ues.
    + * Decorate another {@link MatchableHandlerMapping} that's configured with a
    + * {@link PathPatternParser} in order to parse and cache String patterns passed
    + * into the {@code match} method.
      *
      * @author Rossen Stoyanchev
      * @since 5.3
    @@ -49,8 +50,8 @@ class PathPatternMatchableHandlerMapping implements MatchableHandlerMapping {
     
     
     	public PathPatternMatchableHandlerMapping(MatchableHandlerMapping delegate) {
    -		Assert.notNull(delegate, "Delegate MatchableHandlerMapping is required.");
    -		Assert.notNull(delegate.getPatternParser(), "PatternParser is required.");
    +		Assert.notNull(delegate, "HandlerMapping to delegate to is required.");
    +		Assert.notNull(delegate.getPatternParser(), "Expected HandlerMapping configured to use PatternParser.");
     		this.delegate = delegate;
     		this.parser = delegate.getPatternParser();
     	}
    

Vulnerability mechanics

Root cause

"Inconsistent path pattern matching between Spring Security and Spring MVC when using `**` allows for security bypasses."

Attack vector

An attacker can bypass security restrictions by crafting a request path that is interpreted differently by Spring Security and Spring MVC [patch_id=23864]. This occurs when the application uses `**` as a pattern in Spring Security configuration via `mvcRequestMatcher`. The mismatch allows a request to be incorrectly authorized or permitted because the security filter chain and the actual handler mapping resolve the path differently. This is a form of path normalization inconsistency.

Affected code

The vulnerability exists in `HandlerMappingIntrospector.java` within the `getMatchableHandlerMapping` method and its helper class `LookupPathMatchableHandlerMapping` [patch_id=23864]. These components are responsible for resolving and matching request paths against configured security patterns. The logic failed to ensure consistent path normalization between Spring Security and Spring MVC when using the `**` pattern.

What the fix does

The patch updates `HandlerMappingIntrospector` to ensure that path matching is performed consistently by normalizing the patterns used in `LookupPathMatchableHandlerMapping` [patch_id=23864]. Specifically, it ensures that patterns are correctly prefixed with a `/` if missing, preventing discrepancies in how `**` patterns are evaluated. By standardizing the `lookupPath` and the pattern format, the security filter chain and the MVC handler mapping now reach the same conclusion about whether a request matches a protected path.

Preconditions

  • configThe application must use Spring Security with `mvcRequestMatcher`.
  • configThe application must use `**` in its security configuration patterns.

Generated on May 11, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.