From 4fb0244b5a03a96db1ca6949f7c4bead77db4243 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 01f828ebb21..cd922c9e660 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 @@ -54,7 +54,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 860d055a0a0..bdf475b0a20 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 @@ -180,6 +180,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 4c01dc9f4cf..735a6c214b1 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 @@ -134,7 +134,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 9786ffe9954..b7c5ecc3597 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());