mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Prevent special prefixes in default view name resolution
This commit updates the default view name generation logic in both Spring WebMVC and Spring WebFlux to prevent "redirect:" and "forward:" (for MVC) prefixes from the incoming request path. Closes gh-36793
This commit is contained in:
committed by
Brian Clozel
parent
9bec52b1ec
commit
7add5243b9
+64
-52
@@ -44,6 +44,7 @@ import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
@@ -61,6 +62,7 @@ import org.springframework.web.reactive.HandlerResultHandler;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
|
||||
import org.springframework.web.reactive.result.HandlerResultHandlerSupport;
|
||||
import org.springframework.web.server.NotAcceptableStatusException;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
@@ -89,6 +91,7 @@ import org.springframework.web.server.ServerWebExchange;
|
||||
* presence of annotations, for example, for {@code @ResponseBody}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ViewResolutionResultHandler extends HandlerResultHandlerSupport implements HandlerResultHandler, Ordered {
|
||||
@@ -256,65 +259,70 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp
|
||||
clazz = FragmentsRendering.class;
|
||||
}
|
||||
|
||||
if (returnValue == NO_VALUE || ClassUtils.isVoidType(clazz)) {
|
||||
viewsMono = resolveViews(getDefaultViewName(exchange), locale);
|
||||
}
|
||||
else if (CharSequence.class.isAssignableFrom(clazz) && !hasModelAnnotation(parameter)) {
|
||||
viewsMono = resolveViews(returnValue.toString(), locale);
|
||||
}
|
||||
else if (Rendering.class.isAssignableFrom(clazz)) {
|
||||
Rendering render = (Rendering) returnValue;
|
||||
HttpStatusCode status = render.status();
|
||||
if (status != null) {
|
||||
exchange.getResponse().setStatusCode(status);
|
||||
try {
|
||||
if (returnValue == NO_VALUE || ClassUtils.isVoidType(clazz)) {
|
||||
viewsMono = resolveViews(getDefaultViewName(exchange), locale);
|
||||
}
|
||||
exchange.getResponse().getHeaders().putAll(render.headers());
|
||||
model.addAllAttributes(render.modelAttributes());
|
||||
Object view = render.view();
|
||||
if (view == null) {
|
||||
view = getDefaultViewName(exchange);
|
||||
else if (CharSequence.class.isAssignableFrom(clazz) && !hasModelAnnotation(parameter)) {
|
||||
viewsMono = resolveViews(returnValue.toString(), locale);
|
||||
}
|
||||
viewsMono = (view instanceof String viewName ? resolveViews(viewName, locale) :
|
||||
Mono.just(Collections.singletonList((View) view)));
|
||||
}
|
||||
else if (FragmentsRendering.class.isAssignableFrom(clazz)) {
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
FragmentsRendering render = (FragmentsRendering) returnValue;
|
||||
HttpStatusCode status = render.status();
|
||||
if (status != null) {
|
||||
response.setStatusCode(status);
|
||||
else if (Rendering.class.isAssignableFrom(clazz)) {
|
||||
Rendering render = (Rendering) returnValue;
|
||||
HttpStatusCode status = render.status();
|
||||
if (status != null) {
|
||||
exchange.getResponse().setStatusCode(status);
|
||||
}
|
||||
exchange.getResponse().getHeaders().putAll(render.headers());
|
||||
model.addAllAttributes(render.modelAttributes());
|
||||
Object view = render.view();
|
||||
if (view == null) {
|
||||
view = getDefaultViewName(exchange);
|
||||
}
|
||||
viewsMono = (view instanceof String viewName ? resolveViews(viewName, locale) :
|
||||
Mono.just(Collections.singletonList((View) view)));
|
||||
}
|
||||
response.getHeaders().putAll(render.headers());
|
||||
bindingContext.updateModel(exchange);
|
||||
else if (FragmentsRendering.class.isAssignableFrom(clazz)) {
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
FragmentsRendering render = (FragmentsRendering) returnValue;
|
||||
HttpStatusCode status = render.status();
|
||||
if (status != null) {
|
||||
response.setStatusCode(status);
|
||||
}
|
||||
response.getHeaders().putAll(render.headers());
|
||||
bindingContext.updateModel(exchange);
|
||||
|
||||
StreamHandler streamHandler =
|
||||
(this.sseHandler.supports(exchange.getRequest()) ? this.sseHandler : null);
|
||||
StreamHandler streamHandler =
|
||||
(this.sseHandler.supports(exchange.getRequest()) ? this.sseHandler : null);
|
||||
|
||||
if (streamHandler != null) {
|
||||
streamHandler.updateResponse(exchange);
|
||||
if (streamHandler != null) {
|
||||
streamHandler.updateResponse(exchange);
|
||||
}
|
||||
|
||||
Flux<Flux<DataBuffer>> renderFlux = render.fragments()
|
||||
.concatMap(fragment -> renderFragment(fragment, null, streamHandler, locale, bindingContext, exchange))
|
||||
.doOnDiscard(DataBuffer.class, DataBufferUtils::release);
|
||||
|
||||
return response.writeAndFlushWith(renderFlux);
|
||||
}
|
||||
else if (Model.class.isAssignableFrom(clazz)) {
|
||||
model.addAllAttributes(((Model) returnValue).asMap());
|
||||
viewsMono = resolveViews(getDefaultViewName(exchange), locale);
|
||||
}
|
||||
else if (Map.class.isAssignableFrom(clazz) && !hasModelAnnotation(parameter)) {
|
||||
model.addAllAttributes((Map<String, ?>) returnValue);
|
||||
viewsMono = resolveViews(getDefaultViewName(exchange), locale);
|
||||
}
|
||||
else if (View.class.isAssignableFrom(clazz)) {
|
||||
viewsMono = Mono.just(Collections.singletonList((View) returnValue));
|
||||
}
|
||||
else {
|
||||
String name = getNameForReturnValue(parameter);
|
||||
model.addAttribute(name, returnValue);
|
||||
viewsMono = resolveViews(getDefaultViewName(exchange), locale);
|
||||
}
|
||||
|
||||
Flux<Flux<DataBuffer>> renderFlux = render.fragments()
|
||||
.concatMap(fragment -> renderFragment(fragment, null, streamHandler, locale, bindingContext, exchange))
|
||||
.doOnDiscard(DataBuffer.class, DataBufferUtils::release);
|
||||
|
||||
return response.writeAndFlushWith(renderFlux);
|
||||
}
|
||||
else if (Model.class.isAssignableFrom(clazz)) {
|
||||
model.addAllAttributes(((Model) returnValue).asMap());
|
||||
viewsMono = resolveViews(getDefaultViewName(exchange), locale);
|
||||
}
|
||||
else if (Map.class.isAssignableFrom(clazz) && !hasModelAnnotation(parameter)) {
|
||||
model.addAllAttributes((Map<String, ?>) returnValue);
|
||||
viewsMono = resolveViews(getDefaultViewName(exchange), locale);
|
||||
}
|
||||
else if (View.class.isAssignableFrom(clazz)) {
|
||||
viewsMono = Mono.just(Collections.singletonList((View) returnValue));
|
||||
}
|
||||
else {
|
||||
String name = getNameForReturnValue(parameter);
|
||||
model.addAttribute(name, returnValue);
|
||||
viewsMono = resolveViews(getDefaultViewName(exchange), locale);
|
||||
catch (ResponseStatusException ex) {
|
||||
return Mono.error(ex);
|
||||
}
|
||||
bindingContext.updateModel(exchange);
|
||||
return viewsMono.flatMap(views -> render(views, model.asMap(), null, bindingContext, exchange));
|
||||
@@ -324,12 +332,16 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp
|
||||
/**
|
||||
* Select a default view name when a controller did not specify it.
|
||||
* Use the request path the leading and trailing slash stripped.
|
||||
* @throws ResponseStatusException with a 400 error code if the path contains a "redirect:" prefix
|
||||
*/
|
||||
private String getDefaultViewName(ServerWebExchange exchange) {
|
||||
String path = exchange.getRequest().getPath().pathWithinApplication().value();
|
||||
if (path.startsWith("/")) {
|
||||
path = path.substring(1);
|
||||
}
|
||||
if (path.startsWith(UrlBasedViewResolver.REDIRECT_URL_PREFIX)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Rejected path '" + path + "' with 'redirect:' prefix");
|
||||
}
|
||||
if (path.endsWith("/")) {
|
||||
path = path.substring(0, path.length() - 1);
|
||||
}
|
||||
|
||||
+15
@@ -52,6 +52,7 @@ import org.springframework.web.reactive.HandlerResult;
|
||||
import org.springframework.web.reactive.accept.HeaderContentTypeResolver;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
|
||||
import org.springframework.web.server.NotAcceptableStatusException;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpResponse;
|
||||
import org.springframework.web.testfixture.server.MockServerWebExchange;
|
||||
@@ -256,6 +257,20 @@ class ViewResolutionResultHandlerTests {
|
||||
assertResponseBody(exchange, "account: {id=123}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultViewNameWithRedirectPrefixFails() {
|
||||
MethodParameter returnType = on(Handler.class).resolveReturnType(Mono.class, String.class);
|
||||
HandlerResult result = new HandlerResult(new Object(), Mono.empty(), returnType, this.bindingContext);
|
||||
ViewResolutionResultHandler handler = resultHandler(new TestViewResolver("account"));
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(get("/redirect:account"));
|
||||
Mono<Void> mono = handler.handleResult(exchange, result);
|
||||
StepVerifier.create(mono)
|
||||
.expectNextCount(0)
|
||||
.expectError(ResponseStatusException.class)
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void unresolvedViewName() {
|
||||
String returnValue = "account";
|
||||
|
||||
+13
-2
@@ -20,7 +20,9 @@ import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.servlet.RequestToViewNameTranslator;
|
||||
import org.springframework.web.util.ServletRequestPathUtils;
|
||||
|
||||
@@ -50,6 +52,7 @@ import org.springframework.web.util.ServletRequestPathUtils;
|
||||
*
|
||||
* @author Rob Harrop
|
||||
* @author Juergen Hoeller
|
||||
* @author Sebastien Deleuze
|
||||
* @since 2.0
|
||||
* @see org.springframework.web.servlet.RequestToViewNameTranslator
|
||||
* @see org.springframework.web.servlet.ViewResolver
|
||||
@@ -127,13 +130,21 @@ public class DefaultRequestToViewNameTranslator implements RequestToViewNameTran
|
||||
* into the view name based on the configured parameters.
|
||||
* @throws IllegalArgumentException if neither a parsed RequestPath, nor a
|
||||
* String lookupPath have been resolved and cached as a request attribute.
|
||||
* @throws ResponseStatusException with a 400 error code if the path contains a "redirect:" or a "forward:" prefix
|
||||
* @see ServletRequestPathUtils#getCachedPath(ServletRequest)
|
||||
* @see #transformPath
|
||||
*/
|
||||
@Override
|
||||
public String getViewName(HttpServletRequest request) {
|
||||
String path = ServletRequestPathUtils.getCachedPathValue(request);
|
||||
return (this.prefix + transformPath(path) + this.suffix);
|
||||
String viewName = this.prefix + transformPath(path) + this.suffix;
|
||||
if (viewName.startsWith(UrlBasedViewResolver.REDIRECT_URL_PREFIX)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Rejected path '" + path + "' with 'redirect:' prefix");
|
||||
}
|
||||
if (viewName.startsWith(UrlBasedViewResolver.FORWARD_URL_PREFIX)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Rejected path '" + path + "' with 'forward:' prefix");
|
||||
}
|
||||
return viewName;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,7 +155,7 @@ public class DefaultRequestToViewNameTranslator implements RequestToViewNameTran
|
||||
* @return the transformed path, with slashes and extensions stripped
|
||||
* if desired
|
||||
*/
|
||||
protected @Nullable String transformPath(String lookupPath) {
|
||||
protected String transformPath(String lookupPath) {
|
||||
String path = lookupPath;
|
||||
if (this.stripLeadingSlash && path.startsWith(SLASH)) {
|
||||
path = path.substring(1);
|
||||
|
||||
+20
@@ -21,15 +21,19 @@ import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.Named;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.servlet.handler.PathPatternsParameterizedTest;
|
||||
import org.springframework.web.servlet.handler.PathPatternsTestUtils;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* @author Rick Evans
|
||||
* @author Juergen Hoeller
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
class DefaultRequestToViewNameTranslatorTests {
|
||||
|
||||
@@ -121,6 +125,22 @@ class DefaultRequestToViewNameTranslatorTests {
|
||||
assertViewName(request, VIEW_NAME);
|
||||
}
|
||||
|
||||
@PathPatternsParameterizedTest
|
||||
void getViewNameWithRedirectPrefixFails(Function<String, MockHttpServletRequest> requestFactory) {
|
||||
MockHttpServletRequest request = requestFactory.apply(UrlBasedViewResolver.REDIRECT_URL_PREFIX + VIEW_NAME);
|
||||
assertThatExceptionOfType(ResponseStatusException.class)
|
||||
.isThrownBy(() -> this.translator.getViewName(request))
|
||||
.satisfies(ex -> assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST));
|
||||
}
|
||||
|
||||
@PathPatternsParameterizedTest
|
||||
void getViewNameWithForwardPrefixFails(Function<String, MockHttpServletRequest> requestFactory) {
|
||||
MockHttpServletRequest request = requestFactory.apply(UrlBasedViewResolver.FORWARD_URL_PREFIX + VIEW_NAME);
|
||||
assertThatExceptionOfType(ResponseStatusException.class)
|
||||
.isThrownBy(() -> this.translator.getViewName(request))
|
||||
.satisfies(ex -> assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST));
|
||||
}
|
||||
|
||||
|
||||
private void assertViewName(MockHttpServletRequest request, String expectedViewName) {
|
||||
String actualViewName = this.translator.getViewName(request);
|
||||
|
||||
Reference in New Issue
Block a user