From 8d51f47357e4830076b9630ffc8bfbb2a2831260 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 23 Apr 2026 15:04:02 +0200 Subject: [PATCH] Enfoce single version removal in content versioning Prior to this commit, content based version strategies would remove all instances of the version string in the request path when trying to resolve the original resource with the chain. This can cause issues in rare cases where there is a collision between the content version and some other version string in the request path. Because this strategy is based on the contents of the file itself, we should only remove the last instance of the version string and then attempt to resolve the original file. Fixes gh-36698 --- .../resource/AbstractFileNameVersionStrategy.java | 7 ++++++- .../web/reactive/resource/VersionResourceResolver.java | 3 +++ .../resource/ContentBasedVersionStrategyTests.java | 8 ++++++++ .../web/servlet/resource/AbstractVersionStrategy.java | 7 ++++++- .../web/servlet/resource/VersionResourceResolver.java | 3 +++ .../resource/ContentBasedVersionStrategyTests.java | 8 ++++++++ 6 files changed, 34 insertions(+), 2 deletions(-) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/AbstractFileNameVersionStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/AbstractFileNameVersionStrategy.java index 55a39d292e9..3028f0a1532 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/AbstractFileNameVersionStrategy.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/AbstractFileNameVersionStrategy.java @@ -55,7 +55,12 @@ public abstract class AbstractFileNameVersionStrategy implements VersionStrategy @Override public String removeVersion(String requestPath, String version) { - return StringUtils.delete(requestPath, "-" + version); + String versionString = "-" + version; + int index = requestPath.lastIndexOf(versionString); + if (index != -1) { + return requestPath.substring(0, index) + requestPath.substring(index + versionString.length()); + } + return requestPath; } @Override diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java index 26af9efa11c..5c8626b4331 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/VersionResourceResolver.java @@ -179,6 +179,9 @@ public class VersionResourceResolver extends AbstractResourceResolver { } String simplePath = versionStrategy.removeVersion(requestPath, candidate); + if (ResourceHandlerUtils.shouldIgnoreInputPath(simplePath)) { + return Mono.empty(); + } return chain.resolveResource(exchange, simplePath, locations) .filterWhen(resource -> versionStrategy.getResourceVersion(resource) .map(actual -> { diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ContentBasedVersionStrategyTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ContentBasedVersionStrategyTests.java index af983468517..0bf9e9dec58 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ContentBasedVersionStrategyTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ContentBasedVersionStrategyTests.java @@ -62,6 +62,14 @@ class ContentBasedVersionStrategyTests { assertThat(this.strategy.removeVersion(String.format(path, "-", hash), hash)).isEqualTo(String.format(path, "", "")); } + @Test + void removeVersionOnlyOnce() { + String hash = "sha"; + String path = "font-awesome/css%s%s/font-awesome.min%s%s.css"; + + assertThat(this.strategy.removeVersion(String.format(path, "-", hash, "-", hash), hash)).isEqualTo(String.format(path, "-", hash, "", "")); + } + @Test void getResourceVersion() throws Exception { Resource expected = new ClassPathResource("test/bar.css", getClass()); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AbstractVersionStrategy.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AbstractVersionStrategy.java index b6c4d2b547b..2b7bfc90d46 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AbstractVersionStrategy.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AbstractVersionStrategy.java @@ -137,7 +137,12 @@ public abstract class AbstractVersionStrategy implements VersionStrategy { @Override public String removeVersion(String requestPath, String version) { - return StringUtils.delete(requestPath, "-" + version); + String versionString = "-" + version; + int index = requestPath.lastIndexOf(versionString); + if (index != -1) { + return requestPath.substring(0, index) + requestPath.substring(index + versionString.length()); + } + return requestPath; } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java index 3ea2063c7e3..8491110ee3e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java @@ -177,6 +177,9 @@ public class VersionResourceResolver extends AbstractResourceResolver { } String simplePath = versionStrategy.removeVersion(requestPath, candidateVersion); + if (ResourceHandlerUtils.shouldIgnoreInputPath(simplePath)) { + return null; + } Resource baseResource = chain.resolveResource(request, simplePath, locations); if (baseResource == null) { return null; diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ContentBasedVersionStrategyTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ContentBasedVersionStrategyTests.java index b0a9a4fb547..96144db1103 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ContentBasedVersionStrategyTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/ContentBasedVersionStrategyTests.java @@ -63,6 +63,14 @@ class ContentBasedVersionStrategyTests { assertThat(this.versionStrategy.removeVersion(String.format(file, "-", hash), hash)).isEqualTo(String.format(file, "", "")); } + @Test + void removeVersionOnlyOnce() { + String hash = "sha"; + String file = "font-awesome/css%s%s/font-awesome.min%s%s.css"; + + assertThat(this.versionStrategy.removeVersion(String.format(file, "-", hash, "-", hash), hash)).isEqualTo(String.format(file, "-", hash, "", "")); + } + @Test void getResourceVersion() throws IOException { Resource expected = new ClassPathResource("test/bar.css", getClass());