VYPR
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.

PackageAffected versionsPatched versions
org.springframework:spring-webMaven
< 5.3.385.3.38
org.springframework:spring-webMaven
>= 6.0.0, < 6.0.236.0.23
org.springframework:spring-webMaven
>= 6.1.0, < 6.1.126.1.12

Patches

3
582bfccbb72e

Efficient 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;
     				}
    
8d16a50907c1

Efficient 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\"";
    
bb17ad8314b8

Efficient 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

News mentions

0

No linked articles in our index yet.