diff --git a/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java index a299784a6a1..0568b90465d 100644 --- a/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java +++ b/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java @@ -60,7 +60,17 @@ public abstract class AbstractMappingContentNegotiationStrategy public List resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException { - String key = getMediaTypeKey(webRequest); + return resolveMediaTypeKey(webRequest, getMediaTypeKey(webRequest)); + } + + /** + * An alternative to {@link #resolveMediaTypes(NativeWebRequest)} that accepts + * an already extracted key. + * @since 3.2.16 + */ + public List resolveMediaTypeKey(NativeWebRequest webRequest, String key) + throws HttpMediaTypeNotAcceptableException { + if (StringUtils.hasText(key)) { MediaType mediaType = lookupMediaType(key); if (mediaType != null) { diff --git a/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManager.java b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManager.java index eeba53f7d9c..4450ad10649 100644 --- a/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManager.java +++ b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManager.java @@ -88,6 +88,14 @@ public class ContentNegotiationManager implements ContentNegotiationStrategy, } + /** + * Return the configured content negotiation strategies. + * @since 3.2.16 + */ + public List getStrategies() { + return this.strategies; + } + /** * Register more {@code MediaTypeFileExtensionResolver} instances in addition * to those detected at construction. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java index 57be8f0ac71..3f2439ed261 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java @@ -43,6 +43,8 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.accept.ContentNegotiationManager; +import org.springframework.web.accept.ContentNegotiationStrategy; +import org.springframework.web.accept.PathExtensionContentNegotiationStrategy; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; @@ -77,12 +79,18 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe "json", "xml", "atom", "rss", "png", "jpe", "jpeg", "jpg", "gif", "wbmp", "bmp")); + private static final Set WHITELISTED_MEDIA_BASE_TYPES = new HashSet( + Arrays.asList("audio", "image", "video")); + private final ContentNegotiationManager contentNegotiationManager; + private final PathExtensionContentNegotiationStrategy pathStrategy; + private final Set safeExtensions = new HashSet(); + /** * Constructor with list of converters only. */ @@ -108,10 +116,20 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe super(converters, requestResponseBodyAdvice); this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager()); + this.pathStrategy = initPathStrategy(this.contentNegotiationManager); this.safeExtensions.addAll(this.contentNegotiationManager.getAllFileExtensions()); this.safeExtensions.addAll(WHITELISTED_EXTENSIONS); } + private static PathExtensionContentNegotiationStrategy initPathStrategy(ContentNegotiationManager manager) { + for (ContentNegotiationStrategy strategy : manager.getStrategies()) { + if (strategy instanceof PathExtensionContentNegotiationStrategy) { + return (PathExtensionContentNegotiationStrategy) strategy; + } + } + return new PathExtensionContentNegotiationStrategy(); + } + /** * Creates a new {@link HttpOutputMessage} from the given {@link NativeWebRequest}. @@ -386,7 +404,31 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe return true; } } - return false; + return safeMediaTypesForExtension(extension); + } + + private boolean safeMediaTypesForExtension(String extension) { + List mediaTypes = null; + try { + mediaTypes = this.pathStrategy.resolveMediaTypeKey(null, extension); + } + catch (HttpMediaTypeNotAcceptableException e) { + // Ignore + } + if (CollectionUtils.isEmpty(mediaTypes)) { + return false; + } + for (MediaType mediaType : mediaTypes) { + if (!safeMediaType(mediaType)) { + return false; + } + } + return true; + } + + private boolean safeMediaType(MediaType mediaType) { + return (WHITELISTED_MEDIA_BASE_TYPES.contains(mediaType.getType()) || + mediaType.getSubtype().endsWith("+xml")); } }