Medium severity5.3NVD Advisory· Published Sep 27, 2024· Updated Apr 15, 2026
CVE-2024-38809
CVE-2024-38809
Description
Applications that parse ETags from "If-Match" or "If-None-Match" request headers are vulnerable to DoS attack.
Users of affected versions should upgrade to the corresponding fixed version.
Users of older, unsupported versions could enforce a size limit on "If-Match" and "If-None-Match" headers, e.g. through a Filter.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.springframework:spring-webMaven | < 5.3.38 | 5.3.38 |
org.springframework:spring-webMaven | >= 6.0.0, < 6.0.23 | 6.0.23 |
org.springframework:spring-webMaven | >= 6.1.0, < 6.1.12 | 6.1.12 |
Patches
3582bfccbb72eEfficient ETag parsing
3 files changed · +180 −43
spring-web/src/main/java/org/springframework/http/ETag.java+161 −0 added@@ -0,0 +1,161 @@ +/* + * Copyright 2002-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.StringUtils; + +/** + * Represents an ETag for HTTP conditional requests. + * + * @author Rossen Stoyanchev + * @since 5.3.38 + * @see <a href="https://datatracker.ietf.org/doc/html/rfc7232">RFC 7232</a> + */ +public class ETag { + + private static final Log logger = LogFactory.getLog(ETag.class); + + private static final ETag WILDCARD = new ETag("*", false); + + + private final String tag; + + private final boolean weak; + + + public ETag(String tag, boolean weak) { + this.tag = tag; + this.weak = weak; + } + + + public String tag() { + return this.tag; + } + + public boolean weak() { + return this.weak; + } + + /** + * Whether this a wildcard tag matching to any entity tag value. + */ + public boolean isWildcard() { + return (this == WILDCARD); + } + + /** + * Return the fully formatted tag including "W/" prefix and quotes. + */ + public String formattedTag() { + if (this == WILDCARD) { + return "*"; + } + return (this.weak ? "W/" : "") + "\"" + this.tag + "\""; + } + + @Override + public String toString() { + return formattedTag(); + } + + + /** + * Parse entity tags from an "If-Match" or "If-None-Match" header. + * @param source the source string to parse + * @return the parsed ETags + */ + public static List<ETag> parse(String source) { + + List<ETag> result = new ArrayList<>(); + State state = State.BEFORE_QUOTES; + int startIndex = -1; + boolean weak = false; + + for (int i = 0; i < source.length(); i++) { + char c = source.charAt(i); + + if (state == State.IN_QUOTES) { + if (c == '"') { + String tag = source.substring(startIndex, i); + if (StringUtils.hasText(tag)) { + result.add(new ETag(tag, weak)); + } + state = State.AFTER_QUOTES; + startIndex = -1; + weak = false; + } + continue; + } + + if (Character.isWhitespace(c)) { + continue; + } + + if (c == ',') { + state = State.BEFORE_QUOTES; + continue; + } + + if (state == State.BEFORE_QUOTES) { + if (c == '*') { + result.add(WILDCARD); + state = State.AFTER_QUOTES; + continue; + } + if (c == '"') { + state = State.IN_QUOTES; + startIndex = i + 1; + continue; + } + if (c == 'W' && source.length() > i + 2) { + if (source.charAt(i + 1) == '/' && source.charAt(i + 2) == '"') { + state = State.IN_QUOTES; + i = i + 2; + startIndex = i + 1; + weak = true; + continue; + } + } + } + + if (logger.isDebugEnabled()) { + logger.debug("Unexpected char at index " + i); + } + } + + if (state != State.IN_QUOTES && logger.isDebugEnabled()) { + logger.debug("Expected closing '\"'"); + } + + return result; + } + + + private enum State { + + BEFORE_QUOTES, IN_QUOTES, AFTER_QUOTES + + } + +}
spring-web/src/main/java/org/springframework/http/HttpHeaders.java+14 −30 modified@@ -40,8 +40,6 @@ import java.util.Map; import java.util.Set; import java.util.StringJoiner; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; import org.springframework.lang.Nullable; @@ -393,12 +391,6 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable */ public static final HttpHeaders EMPTY = new ReadOnlyHttpHeaders(new LinkedMultiValueMap<>()); - /** - * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match". - * @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a> - */ - private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?"); - private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = new DecimalFormatSymbols(Locale.ENGLISH); private static final ZoneId GMT = ZoneId.of("GMT"); @@ -1568,35 +1560,27 @@ public void clearContentHeaders() { /** * Retrieve a combined result from the field values of the ETag header. - * @param headerName the header name + * @param name the header name * @return the combined result * @throws IllegalArgumentException if parsing fails * @since 4.3 */ - protected List<String> getETagValuesAsList(String headerName) { - List<String> values = get(headerName); - if (values != null) { - List<String> result = new ArrayList<>(); - for (String value : values) { - if (value != null) { - Matcher matcher = ETAG_HEADER_VALUE_PATTERN.matcher(value); - while (matcher.find()) { - if ("*".equals(matcher.group())) { - result.add(matcher.group()); - } - else { - result.add(matcher.group(1)); - } - } - if (result.isEmpty()) { - throw new IllegalArgumentException( - "Could not parse header '" + headerName + "' with value '" + value + "'"); - } + protected List<String> getETagValuesAsList(String name) { + List<String> values = get(name); + if (values == null) { + return Collections.emptyList(); + } + List<String> result = new ArrayList<>(); + for (String value : values) { + if (value != null) { + List<ETag> tags = ETag.parse(value); + Assert.notEmpty(tags, "Could not parse header '" + name + "' with value '" + value + "'"); + for (ETag tag : tags) { + result.add(tag.formattedTag()); } } - return result; } - return Collections.emptyList(); + return result; } /**
spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java+5 −13 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -26,13 +26,12 @@ import java.util.Locale; import java.util.Map; import java.util.TimeZone; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import org.springframework.http.ETag; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -54,12 +53,6 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ private static final List<String> SAFE_METHODS = Arrays.asList("GET", "HEAD"); - /** - * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match". - * @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a> - */ - private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?"); - /** * Date formats as specified in the HTTP RFC. * @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a> @@ -289,11 +282,10 @@ private boolean validateIfNoneMatch(@Nullable String etag) { etag = etag.substring(2); } while (ifNoneMatch.hasMoreElements()) { - String clientETags = ifNoneMatch.nextElement(); - Matcher etagMatcher = ETAG_HEADER_VALUE_PATTERN.matcher(clientETags); // Compare weak/strong ETags as per https://tools.ietf.org/html/rfc7232#section-2.3 - while (etagMatcher.find()) { - if (StringUtils.hasLength(etagMatcher.group()) && etag.equals(etagMatcher.group(3))) { + for (ETag requestedETag : ETag.parse(ifNoneMatch.nextElement())) { + String tag = requestedETag.tag(); + if (StringUtils.hasLength(tag) && etag.equals(padEtagIfNecessary(tag))) { this.notModified = true; break; }
8d16a50907c1Efficient ETag parsing
4 files changed · +166 −46
spring-web/src/main/java/org/springframework/http/ETag.java+144 −0 added@@ -0,0 +1,144 @@ +/* + * Copyright 2002-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.StringUtils; + +/** + * Represents an ETag for HTTP conditional requests. + * + * @param tag the unquoted tag value + * @param weak whether the entity tag is for weak or strong validation + * @author Rossen Stoyanchev + * @since 5.3.38 + * @see <a href="https://datatracker.ietf.org/doc/html/rfc7232">RFC 7232</a> + */ +public record ETag(String tag, boolean weak) { + + private static final Log logger = LogFactory.getLog(ETag.class); + + private static final ETag WILDCARD = new ETag("*", false); + + + /** + * Whether this a wildcard tag matching to any entity tag value. + */ + public boolean isWildcard() { + return (this == WILDCARD); + } + + /** + * Return the fully formatted tag including "W/" prefix and quotes. + */ + public String formattedTag() { + if (isWildcard()) { + return "*"; + } + return (this.weak ? "W/" : "") + "\"" + this.tag + "\""; + } + + @Override + public String toString() { + return formattedTag(); + } + + + /** + * Parse entity tags from an "If-Match" or "If-None-Match" header. + * @param source the source string to parse + * @return the parsed ETags + */ + public static List<ETag> parse(String source) { + + List<ETag> result = new ArrayList<>(); + State state = State.BEFORE_QUOTES; + int startIndex = -1; + boolean weak = false; + + for (int i = 0; i < source.length(); i++) { + char c = source.charAt(i); + + if (state == State.IN_QUOTES) { + if (c == '"') { + String tag = source.substring(startIndex, i); + if (StringUtils.hasText(tag)) { + result.add(new ETag(tag, weak)); + } + state = State.AFTER_QUOTES; + startIndex = -1; + weak = false; + } + continue; + } + + if (Character.isWhitespace(c)) { + continue; + } + + if (c == ',') { + state = State.BEFORE_QUOTES; + continue; + } + + if (state == State.BEFORE_QUOTES) { + if (c == '*') { + result.add(WILDCARD); + state = State.AFTER_QUOTES; + continue; + } + if (c == '"') { + state = State.IN_QUOTES; + startIndex = i + 1; + continue; + } + if (c == 'W' && source.length() > i + 2) { + if (source.charAt(i + 1) == '/' && source.charAt(i + 2) == '"') { + state = State.IN_QUOTES; + i = i + 2; + startIndex = i + 1; + weak = true; + continue; + } + } + } + + if (logger.isDebugEnabled()) { + logger.debug("Unexpected char at index " + i); + } + } + + if (state != State.IN_QUOTES && logger.isDebugEnabled()) { + logger.debug("Expected closing '\"'"); + } + + return result; + } + + + private enum State { + + BEFORE_QUOTES, IN_QUOTES, AFTER_QUOTES + + } + +}
spring-web/src/main/java/org/springframework/http/HttpHeaders.java+14 −30 modified@@ -41,8 +41,6 @@ import java.util.Set; import java.util.StringJoiner; import java.util.function.BiConsumer; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; import org.springframework.lang.Nullable; @@ -394,12 +392,6 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable */ public static final HttpHeaders EMPTY = new ReadOnlyHttpHeaders(new LinkedMultiValueMap<>()); - /** - * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match". - * @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a> - */ - private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?"); - private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = new DecimalFormatSymbols(Locale.ENGLISH); private static final ZoneId GMT = ZoneId.of("GMT"); @@ -1625,35 +1617,27 @@ public void clearContentHeaders() { /** * Retrieve a combined result from the field values of the ETag header. - * @param headerName the header name + * @param name the header name * @return the combined result * @throws IllegalArgumentException if parsing fails * @since 4.3 */ - protected List<String> getETagValuesAsList(String headerName) { - List<String> values = get(headerName); - if (values != null) { - List<String> result = new ArrayList<>(); - for (String value : values) { - if (value != null) { - Matcher matcher = ETAG_HEADER_VALUE_PATTERN.matcher(value); - while (matcher.find()) { - if ("*".equals(matcher.group())) { - result.add(matcher.group()); - } - else { - result.add(matcher.group(1)); - } - } - if (result.isEmpty()) { - throw new IllegalArgumentException( - "Could not parse header '" + headerName + "' with value '" + value + "'"); - } + protected List<String> getETagValuesAsList(String name) { + List<String> values = get(name); + if (values == null) { + return Collections.emptyList(); + } + List<String> result = new ArrayList<>(); + for (String value : values) { + if (value != null) { + List<ETag> tags = ETag.parse(value); + Assert.notEmpty(tags, "Could not parse header '" + name + "' with value '" + value + "'"); + for (ETag tag : tags) { + result.add(tag.formattedTag()); } } - return result; } - return Collections.emptyList(); + return result; } /**
spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java+6 −14 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -25,13 +25,12 @@ import java.util.Map; import java.util.Set; import java.util.TimeZone; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; +import org.springframework.http.ETag; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -53,12 +52,6 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ private static final Set<String> SAFE_METHODS = Set.of("GET", "HEAD"); - /** - * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match". - * @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a> - */ - private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?"); - /** * Date formats as specified in the HTTP RFC. * @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a> @@ -255,20 +248,19 @@ private boolean matchRequestedETags(Enumeration<String> requestedETags, @Nullabl eTag = padEtagIfNecessary(eTag); while (requestedETags.hasMoreElements()) { // Compare weak/strong ETags as per https://datatracker.ietf.org/doc/html/rfc9110#section-8.8.3 - Matcher eTagMatcher = ETAG_HEADER_VALUE_PATTERN.matcher(requestedETags.nextElement()); - while (eTagMatcher.find()) { + for (ETag requestedETag : ETag.parse(requestedETags.nextElement())) { // only consider "lost updates" checks for unsafe HTTP methods - if ("*".equals(eTagMatcher.group()) && StringUtils.hasLength(eTag) + if (requestedETag.isWildcard() && StringUtils.hasLength(eTag) && !SAFE_METHODS.contains(getRequest().getMethod())) { return false; } if (weakCompare) { - if (eTagWeakMatch(eTag, eTagMatcher.group(1))) { + if (eTagWeakMatch(eTag, requestedETag.formattedTag())) { return false; } } else { - if (eTagStrongMatch(eTag, eTagMatcher.group(1))) { + if (eTagStrongMatch(eTag, requestedETag.formattedTag())) { return false; } }
spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java+2 −2 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -163,8 +163,8 @@ void ifNoneMatchShouldNotMatchDifferentETag(String method) { assertOkWithETag(etag); } + // gh-19127 @SafeHttpMethodsTest - // SPR-14559 void ifNoneMatchShouldNotFailForUnquotedETag(String method) { setUpRequest(method); String etag = "\"etagvalue\"";
bb17ad8314b8Efficient ETag parsing
4 files changed · +165 −45
spring-web/src/main/java/org/springframework/http/ETag.java+144 −0 added@@ -0,0 +1,144 @@ +/* + * Copyright 2002-2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.StringUtils; + +/** + * Represents an ETag for HTTP conditional requests. + * + * @param tag the unquoted tag value + * @param weak whether the entity tag is for weak or strong validation + * @author Rossen Stoyanchev + * @since 5.3.38 + * @see <a href="https://datatracker.ietf.org/doc/html/rfc7232">RFC 7232</a> + */ +public record ETag(String tag, boolean weak) { + + private static final Log logger = LogFactory.getLog(ETag.class); + + private static final ETag WILDCARD = new ETag("*", false); + + + /** + * Whether this a wildcard tag matching to any entity tag value. + */ + public boolean isWildcard() { + return (this == WILDCARD); + } + + /** + * Return the fully formatted tag including "W/" prefix and quotes. + */ + public String formattedTag() { + if (isWildcard()) { + return "*"; + } + return (this.weak ? "W/" : "") + "\"" + this.tag + "\""; + } + + @Override + public String toString() { + return formattedTag(); + } + + + /** + * Parse entity tags from an "If-Match" or "If-None-Match" header. + * @param source the source string to parse + * @return the parsed ETags + */ + public static List<ETag> parse(String source) { + + List<ETag> result = new ArrayList<>(); + State state = State.BEFORE_QUOTES; + int startIndex = -1; + boolean weak = false; + + for (int i = 0; i < source.length(); i++) { + char c = source.charAt(i); + + if (state == State.IN_QUOTES) { + if (c == '"') { + String tag = source.substring(startIndex, i); + if (StringUtils.hasText(tag)) { + result.add(new ETag(tag, weak)); + } + state = State.AFTER_QUOTES; + startIndex = -1; + weak = false; + } + continue; + } + + if (Character.isWhitespace(c)) { + continue; + } + + if (c == ',') { + state = State.BEFORE_QUOTES; + continue; + } + + if (state == State.BEFORE_QUOTES) { + if (c == '*') { + result.add(WILDCARD); + state = State.AFTER_QUOTES; + continue; + } + if (c == '"') { + state = State.IN_QUOTES; + startIndex = i + 1; + continue; + } + if (c == 'W' && source.length() > i + 2) { + if (source.charAt(i + 1) == '/' && source.charAt(i + 2) == '"') { + state = State.IN_QUOTES; + i = i + 2; + startIndex = i + 1; + weak = true; + continue; + } + } + } + + if (logger.isDebugEnabled()) { + logger.debug("Unexpected char at index " + i); + } + } + + if (state != State.IN_QUOTES && logger.isDebugEnabled()) { + logger.debug("Expected closing '\"'"); + } + + return result; + } + + + private enum State { + + BEFORE_QUOTES, IN_QUOTES, AFTER_QUOTES + + } + +}
spring-web/src/main/java/org/springframework/http/HttpHeaders.java+14 −30 modified@@ -41,8 +41,6 @@ import java.util.Set; import java.util.StringJoiner; import java.util.function.BiConsumer; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; import org.springframework.lang.Nullable; @@ -394,12 +392,6 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable */ public static final HttpHeaders EMPTY = new ReadOnlyHttpHeaders(new LinkedMultiValueMap<>()); - /** - * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match". - * @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a> - */ - private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?"); - private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = new DecimalFormatSymbols(Locale.ENGLISH); private static final ZoneId GMT = ZoneId.of("GMT"); @@ -1629,35 +1621,27 @@ public void clearContentHeaders() { /** * Retrieve a combined result from the field values of the ETag header. - * @param headerName the header name + * @param name the header name * @return the combined result * @throws IllegalArgumentException if parsing fails * @since 4.3 */ - protected List<String> getETagValuesAsList(String headerName) { - List<String> values = get(headerName); - if (values != null) { - List<String> result = new ArrayList<>(); - for (String value : values) { - if (value != null) { - Matcher matcher = ETAG_HEADER_VALUE_PATTERN.matcher(value); - while (matcher.find()) { - if ("*".equals(matcher.group())) { - result.add(matcher.group()); - } - else { - result.add(matcher.group(1)); - } - } - if (result.isEmpty()) { - throw new IllegalArgumentException( - "Could not parse header '" + headerName + "' with value '" + value + "'"); - } + protected List<String> getETagValuesAsList(String name) { + List<String> values = get(name); + if (values == null) { + return Collections.emptyList(); + } + List<String> result = new ArrayList<>(); + for (String value : values) { + if (value != null) { + List<ETag> tags = ETag.parse(value); + Assert.notEmpty(tags, "Could not parse header '" + name + "' with value '" + value + "'"); + for (ETag tag : tags) { + result.add(tag.formattedTag()); } } - return result; } - return Collections.emptyList(); + return result; } /**
spring-web/src/main/java/org/springframework/web/context/request/ServletWebRequest.java+5 −13 modified@@ -25,13 +25,12 @@ import java.util.Map; import java.util.Set; import java.util.TimeZone; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; +import org.springframework.http.ETag; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -53,12 +52,6 @@ public class ServletWebRequest extends ServletRequestAttributes implements Nativ private static final Set<String> SAFE_METHODS = Set.of("GET", "HEAD"); - /** - * Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match". - * @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a> - */ - private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?"); - /** * Date formats as specified in the HTTP RFC. * @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a> @@ -255,20 +248,19 @@ private boolean matchRequestedETags(Enumeration<String> requestedETags, @Nullabl etag = padEtagIfNecessary(etag); while (requestedETags.hasMoreElements()) { // Compare weak/strong ETags as per https://datatracker.ietf.org/doc/html/rfc9110#section-8.8.3 - Matcher etagMatcher = ETAG_HEADER_VALUE_PATTERN.matcher(requestedETags.nextElement()); - while (etagMatcher.find()) { + for (ETag requestedETag : ETag.parse(requestedETags.nextElement())) { // only consider "lost updates" checks for unsafe HTTP methods - if ("*".equals(etagMatcher.group()) && StringUtils.hasLength(etag) + if (requestedETag.isWildcard() && StringUtils.hasLength(etag) && !SAFE_METHODS.contains(getRequest().getMethod())) { return false; } if (weakCompare) { - if (etagWeakMatch(etag, etagMatcher.group(1))) { + if (etagWeakMatch(etag, requestedETag.formattedTag())) { return false; } } else { - if (etagStrongMatch(etag, etagMatcher.group(1))) { + if (etagStrongMatch(etag, requestedETag.formattedTag())) { return false; } }
spring-web/src/test/java/org/springframework/web/context/request/ServletWebRequestHttpMethodsTests.java+2 −2 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -163,8 +163,8 @@ void ifNoneMatchShouldNotMatchDifferentETag(String method) { assertOkWithETag(etag); } + // gh-19127 @SafeHttpMethodsTest - // SPR-14559 void ifNoneMatchShouldNotFailForUnquotedETag(String method) { setUpRequest(method); String etag = "\"etagvalue\"";
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
8- github.com/advisories/GHSA-2rmj-mq67-h97gghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-38809ghsaADVISORY
- github.com/spring-projects/spring-framework/commit/582bfccbb72e5c8959a0b472d1dc7d03a20520f3ghsaWEB
- github.com/spring-projects/spring-framework/commit/8d16a50907c11f7e6b407d878a26e84eba08a533ghsaWEB
- github.com/spring-projects/spring-framework/commit/bb17ad8314b81850a939fd265fb53b3361705e85ghsaWEB
- github.com/spring-projects/spring-framework/issues/33372ghsaWEB
- spring.io/security/cve-2024-38809nvdWEB
- security.netapp.com/advisory/ntap-20240920-0003/nvd
News mentions
0No linked articles in our index yet.