From a2e3c0d81da00aca9c771487f696539c90d9a42a Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 25 Sep 2026 17:06:28 +0200 Subject: [PATCH] Skip invalid links in CssLinkResourceTransformer Prior to this commit, both `CssLinkResourceTransformer` implementations could fail at runtime in case of invalid CSS links (for example, with out of bounds exceptions). This commit skips invalid links and writes them out to the resulting CSS without any transformation. Fixes gh-37336 --- .../resource/CssLinkResourceTransformer.java | 11 ++++++++++- .../CssLinkResourceTransformerTests.java | 19 +++++++++++++++++++ .../resource/test/unclosed_url_function.css | 2 ++ .../resource/CssLinkResourceTransformer.java | 11 ++++++++++- .../CssLinkResourceTransformerTests.java | 14 ++++++++++++++ .../resource/test/unclosed_url_function.css | 2 ++ 6 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 spring-webflux/src/test/resources/org/springframework/web/reactive/resource/test/unclosed_url_function.css create mode 100644 spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/unclosed_url_function.css diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/CssLinkResourceTransformer.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/CssLinkResourceTransformer.java index f612371233f..a8bb5872494 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/CssLinkResourceTransformer.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/CssLinkResourceTransformer.java @@ -176,9 +176,12 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { return; } position += getKeyword().length(); - while (Character.isWhitespace(content.charAt(position))) { + while (position < content.length() && Character.isWhitespace(content.charAt(position))) { position++; } + if (position == content.length()) { + return; + } if (content.charAt(position) == '\'') { position = extractLink(position, '\'', content, result); } @@ -194,6 +197,12 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { protected int extractLink(int index, char endChar, String content, Set result) { int start = index + 1; int end = content.indexOf(endChar, start); + if (end == -1) { + if (logger.isTraceEnabled()) { + logger.trace("Unterminated link at index " + start + ", no closing '" + endChar + "'"); + } + return content.length(); + } result.add(new ContentChunkInfo(start, end, true)); return end + 1; } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/CssLinkResourceTransformerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/CssLinkResourceTransformerTests.java index 8f56b3a0955..7fca7166b97 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/CssLinkResourceTransformerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/CssLinkResourceTransformerTests.java @@ -183,6 +183,25 @@ class CssLinkResourceTransformerTests { .verify(); } + @Test + void transformUnclosedUrlFunction() { + MockServerWebExchange exchange = MockServerWebExchange.from(get("/static/unclosed_url_function.css")); + Resource css = getResource("unclosed_url_function.css"); + String expected = """ + body { background: url("/static/images/image-f448cd1d5dba82b774f3202c878230b3.png?#iefix") } + div { background: url(images/image.png + """; + + StepVerifier.create(this.transformerChain.transform(exchange, css) + .cast(TransformedResource.class)) + .consumeNextWith(transformedResource -> { + String result = new String(transformedResource.getByteArray(), UTF_8); + assertThat(result).isEqualToNormalizingNewlines(expected); + }) + .expectComplete() + .verify(); + } + private Resource getResource(String filePath) { return new ClassPathResource("test/" + filePath, getClass()); } diff --git a/spring-webflux/src/test/resources/org/springframework/web/reactive/resource/test/unclosed_url_function.css b/spring-webflux/src/test/resources/org/springframework/web/reactive/resource/test/unclosed_url_function.css new file mode 100644 index 00000000000..067404446bd --- /dev/null +++ b/spring-webflux/src/test/resources/org/springframework/web/reactive/resource/test/unclosed_url_function.css @@ -0,0 +1,2 @@ +body { background: url("images/image.png?#iefix") } +div { background: url(images/image.png diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CssLinkResourceTransformer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CssLinkResourceTransformer.java index 6b379ab9b05..136d8d2c619 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CssLinkResourceTransformer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CssLinkResourceTransformer.java @@ -140,9 +140,12 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { return; } position += getKeyword().length(); - while (Character.isWhitespace(content.charAt(position))) { + while (position < content.length() && Character.isWhitespace(content.charAt(position))) { position++; } + if (position == content.length()) { + return; + } if (content.charAt(position) == '\'') { position = extractLink(position, "'", content, result); } @@ -158,6 +161,12 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { protected int extractLink(int index, String endKey, String content, SortedSet linksToAdd) { int start = index + 1; int end = content.indexOf(endKey, start); + if (end == -1) { + if (logger.isTraceEnabled()) { + logger.trace("Unterminated link at index " + start + ", no closing \"" + endKey + "\""); + } + return content.length(); + } linksToAdd.add(new ContentChunkInfo(start, end)); return end + endKey.length(); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/CssLinkResourceTransformerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/CssLinkResourceTransformerTests.java index 021ef78e8c2..f5525a9a298 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/CssLinkResourceTransformerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/CssLinkResourceTransformerTests.java @@ -167,6 +167,20 @@ class CssLinkResourceTransformerTests { assertThat(result).isEqualToNormalizingNewlines(expected); } + @Test + void transformUnclosedUrlFunction() throws Exception { + this.request = new MockHttpServletRequest("GET", "/static/unclosed_url_function.css"); + Resource css = getResource("unclosed_url_function.css"); + String expected = """ + body { background: url("/static/images/image-f448cd1d5dba82b774f3202c878230b3.png?#iefix") } + div { background: url(images/image.png + """; + + TransformedResource actual = (TransformedResource) this.transformerChain.transform(this.request, css); + String result = new String(actual.getByteArray(), UTF_8); + assertThat(result).isEqualToNormalizingNewlines(expected); + } + private Resource getResource(String filePath) { return new ClassPathResource("test/" + filePath, getClass()); } diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/unclosed_url_function.css b/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/unclosed_url_function.css new file mode 100644 index 00000000000..067404446bd --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/test/unclosed_url_function.css @@ -0,0 +1,2 @@ +body { background: url("images/image.png?#iefix") } +div { background: url(images/image.png