diff --git a/spring-boot-modules/spring-boot-mvc-5/pom.xml b/spring-boot-modules/spring-boot-mvc-5/pom.xml index a516cab049..10a58a6a59 100644 --- a/spring-boot-modules/spring-boot-mvc-5/pom.xml +++ b/spring-boot-modules/spring-boot-mvc-5/pom.xml @@ -49,6 +49,10 @@ commons-configuration ${commons-configuration.version} + + org.springframework.boot + spring-boot-starter-aop + @@ -61,6 +65,14 @@ JAR + + org.apache.maven.plugins + maven-compiler-plugin + + 9 + 9 + + diff --git a/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/modifyrequest/ModifyRequestApp.java b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/modifyrequest/ModifyRequestApp.java new file mode 100644 index 0000000000..7dd937d5b8 --- /dev/null +++ b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/modifyrequest/ModifyRequestApp.java @@ -0,0 +1,11 @@ +package com.baeldung.modifyrequest; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(scanBasePackages = "com.baeldung.modifyrequest") +public class ModifyRequestApp { + public static void main(String[] args) { + SpringApplication.run(ModifyRequestApp.class, args); + } +} diff --git a/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/modifyrequest/aop/EscapeHtmlAspect.java b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/modifyrequest/aop/EscapeHtmlAspect.java new file mode 100644 index 0000000000..fb31abe11b --- /dev/null +++ b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/modifyrequest/aop/EscapeHtmlAspect.java @@ -0,0 +1,74 @@ +package com.baeldung.modifyrequest.aop; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice; + +import java.io.*; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; + +@RestControllerAdvice +@Profile("aspectExample") +public class EscapeHtmlAspect implements RequestBodyAdvice { + private static final Logger logger = LoggerFactory.getLogger(EscapeHtmlAspect.class); + + @Override + public boolean supports(MethodParameter methodParameter, Type targetType, Class> converterType) { + //Apply this to all Controllers + return true; + } + + @Override + public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class> converterType) throws IOException { + logger.info("beforeBodyRead called"); + InputStream inputStream = inputMessage.getBody(); + return new HttpInputMessage() { + @Override + public InputStream getBody() throws IOException { + return new ByteArrayInputStream(escapeHtml(inputStream).getBytes(StandardCharsets.UTF_8)); + } + + @Override + public HttpHeaders getHeaders() { + return inputMessage.getHeaders(); + } + }; + } + + @Override + public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class> converterType) { + // Return the modified object after reading the body + return body; + } + + @Override + public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class> converterType) { + //return the original body + return body; + } + + private String escapeHtml(InputStream inputStream) throws IOException { + StringBuilder stringBuilder = new StringBuilder(); + BufferedReader bufferedReader = null; + try (inputStream) { + bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + char[] charBuffer = new char[128]; + int bytesRead = -1; + while ((bytesRead = bufferedReader.read(charBuffer)) > 0) { + stringBuilder.append(charBuffer, 0, bytesRead); + } + } + String input = stringBuilder.toString(); + // Escape HTML characters + return input.replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">"); + } +} diff --git a/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/modifyrequest/config/WebMvcConfiguration.java b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/modifyrequest/config/WebMvcConfiguration.java new file mode 100644 index 0000000000..bd76fd5e55 --- /dev/null +++ b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/modifyrequest/config/WebMvcConfiguration.java @@ -0,0 +1,25 @@ +package com.baeldung.modifyrequest.config; + +import com.baeldung.modifyrequest.interceptor.EscapeHtmlRequestInterceptor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@Profile("interceptorExample") +public class WebMvcConfiguration implements WebMvcConfigurer { + private static final Logger logger = LoggerFactory.getLogger(WebMvcConfiguration.class); + + @Override + public void addInterceptors(InterceptorRegistry registry) { + logger.info("addInterceptors() called"); + registry.addInterceptor(new EscapeHtmlRequestInterceptor()) + .addPathPatterns("/save"); + + WebMvcConfigurer.super.addInterceptors(registry); + } +} + diff --git a/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/modifyrequest/controller/UserController.java b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/modifyrequest/controller/UserController.java new file mode 100644 index 0000000000..26450dd70d --- /dev/null +++ b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/modifyrequest/controller/UserController.java @@ -0,0 +1,23 @@ +package com.baeldung.modifyrequest.controller; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/") +public class UserController { + Logger logger = LoggerFactory.getLogger(UserController.class); + + @PostMapping(value = "save") + public ResponseEntity saveUser(@RequestBody String user) { + logger.info("save user info into database"); + ResponseEntity responseEntity = new ResponseEntity<>(user, HttpStatus.CREATED); + return responseEntity; + } +} diff --git a/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/modifyrequest/filter/EscapeHtmlFilter.java b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/modifyrequest/filter/EscapeHtmlFilter.java new file mode 100644 index 0000000000..45cad3be1c --- /dev/null +++ b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/modifyrequest/filter/EscapeHtmlFilter.java @@ -0,0 +1,27 @@ +package com.baeldung.modifyrequest.filter; + +import com.baeldung.modifyrequest.requestwrapper.EscapeHtmlRequestWrapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +@Component +@Order(1) +@Profile("filterExample") +public class EscapeHtmlFilter implements Filter { + Logger logger = LoggerFactory.getLogger(EscapeHtmlFilter.class); + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + logger.info("Modify the request"); + + filterChain.doFilter(new EscapeHtmlRequestWrapper((HttpServletRequest) servletRequest), servletResponse); + } +} diff --git a/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/modifyrequest/interceptor/EscapeHtmlRequestInterceptor.java b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/modifyrequest/interceptor/EscapeHtmlRequestInterceptor.java new file mode 100644 index 0000000000..1ad39605e5 --- /dev/null +++ b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/modifyrequest/interceptor/EscapeHtmlRequestInterceptor.java @@ -0,0 +1,19 @@ +package com.baeldung.modifyrequest.interceptor; + +import com.baeldung.modifyrequest.requestwrapper.EscapeHtmlRequestWrapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class EscapeHtmlRequestInterceptor implements HandlerInterceptor { + private static final Logger logger = LoggerFactory.getLogger(EscapeHtmlRequestInterceptor.class); + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + EscapeHtmlRequestWrapper escapeHtmlRequestWrapper = new EscapeHtmlRequestWrapper(request); + return true; + } +} diff --git a/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/modifyrequest/requestwrapper/EscapeHtmlRequestWrapper.java b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/modifyrequest/requestwrapper/EscapeHtmlRequestWrapper.java new file mode 100644 index 0000000000..65c758d956 --- /dev/null +++ b/spring-boot-modules/spring-boot-mvc-5/src/main/java/com/baeldung/modifyrequest/requestwrapper/EscapeHtmlRequestWrapper.java @@ -0,0 +1,68 @@ +package com.baeldung.modifyrequest.requestwrapper; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.*; + +public class EscapeHtmlRequestWrapper extends HttpServletRequestWrapper { + private String body = null; + public EscapeHtmlRequestWrapper(HttpServletRequest request) throws IOException { + super(request); + this.body = this.escapeHtml(request); + } + + private String escapeHtml(HttpServletRequest request) throws IOException { + StringBuilder stringBuilder = new StringBuilder(); + BufferedReader bufferedReader = null; + try (InputStream inputStream = request.getInputStream()) { + bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + char[] charBuffer = new char[128]; + int bytesRead = -1; + while ((bytesRead = bufferedReader.read(charBuffer)) > 0) { + stringBuilder.append(charBuffer, 0, bytesRead); + } + } + String input = stringBuilder.toString(); + // Escape HTML characters + return input.replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + //.replaceAll("\"", """) + .replaceAll("'", "'"); + } + + @Override + public ServletInputStream getInputStream() { + final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes()); + ServletInputStream servletInputStream = new ServletInputStream() { + + @Override + public int read() { + return byteArrayInputStream.read(); + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener listener) { + + } + }; + return servletInputStream; + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(this.getInputStream())); + } +} diff --git a/spring-boot-modules/spring-boot-mvc-5/src/main/resources/modifyrequest/filter-sequence-design.puml b/spring-boot-modules/spring-boot-mvc-5/src/main/resources/modifyrequest/filter-sequence-design.puml new file mode 100644 index 0000000000..41689ac723 --- /dev/null +++ b/spring-boot-modules/spring-boot-mvc-5/src/main/resources/modifyrequest/filter-sequence-design.puml @@ -0,0 +1,31 @@ +@startuml +'https://plantuml.com/sequence-diagram +skinparam sequenceMessageAlign direction +skinparam handwritten true +skinparam sequence { +ParticipantBackgroundColor beige +ParticipantPadding 50 +} + +autonumber + +Browser -[#63b175]> Filter: HTTP Request +activate Browser +activate Filter +Filter -[#63b175]> Filter: doFilter() +Filter -[#63b175]> DispatcherServlet: HTTP Request +activate DispatcherServlet + + +DispatcherServlet -[#63b175]> Controller: HTTP Request +activate Controller +Controller --[#63b175]> DispatcherServlet: HTTP Response +deactivate Controller + +DispatcherServlet --[#63b175]> Filter: HTTP Response +deactivate DispatcherServlet + +Filter --[#63b175]> Browser: HTTP Response +deactivate Filter +deactivate Browser +@enduml \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-mvc-5/src/main/resources/modifyrequest/interceptor-sequence-design.puml b/spring-boot-modules/spring-boot-mvc-5/src/main/resources/modifyrequest/interceptor-sequence-design.puml new file mode 100644 index 0000000000..429b8182ca --- /dev/null +++ b/spring-boot-modules/spring-boot-mvc-5/src/main/resources/modifyrequest/interceptor-sequence-design.puml @@ -0,0 +1,33 @@ +@startuml +'https://plantuml.com/sequence-diagram +skinparam sequenceMessageAlign direction +skinparam handwritten true +skinparam sequence { +ParticipantBackgroundColor beige +ParticipantPadding 50 +} + +autonumber + +Browser -[#63b175]> Filter: Http Request +activate Browser +activate Filter +Filter -[#63b175]> DispatcherServlet: Http Request +activate DispatcherServlet + +DispatcherServlet -[#63b175]> Interceptor: Http Request +activate Interceptor +Interceptor -[#63b175]> Interceptor: preHandle() +Interceptor -[#63b175]> Controller: Http Request +activate Controller +Controller --[#63b175]> Interceptor: Http Response +deactivate Controller +Interceptor --[#63b175]> DispatcherServlet: Http Response +deactivate Interceptor +DispatcherServlet --[#63b175]> Filter: Http Response +deactivate DispatcherServlet + +Filter --[#63b175]> Browser: Http Response +deactivate Filter +deactivate Browser +@enduml \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-mvc-5/src/test/java/com/baeldung/modifyrequest/EscapeHtmlAspectIntegrationTest.java b/spring-boot-modules/spring-boot-mvc-5/src/test/java/com/baeldung/modifyrequest/EscapeHtmlAspectIntegrationTest.java new file mode 100644 index 0000000000..665733c61b --- /dev/null +++ b/spring-boot-modules/spring-boot-mvc-5/src/test/java/com/baeldung/modifyrequest/EscapeHtmlAspectIntegrationTest.java @@ -0,0 +1,52 @@ +package com.baeldung.modifyrequest; + +import com.baeldung.modifyrequest.controller.UserController; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.net.URI; +import java.util.Map; + +@ExtendWith(SpringExtension.class) +@AutoConfigureMockMvc +@WebMvcTest(UserController.class) +@ActiveProfiles("aspectExample") +public class EscapeHtmlAspectIntegrationTest { + Logger logger = LoggerFactory.getLogger(EscapeHtmlAspectIntegrationTest.class); + + @Autowired + private MockMvc mockMvc; + @Test + void givenAspect_whenEscapeHtmlAspect_thenEscapeHtml() throws Exception { + + Map requestBody = Map.of( + "name", "James Cameron", + "email", "james@gmail.com" + ); + + Map expectedResponseBody = Map.of( + "name", "James Cameron", + "email", "<script>alert()</script>james@gmail.com" + ); + + ObjectMapper objectMapper = new ObjectMapper(); + + mockMvc.perform(MockMvcRequestBuilders.post(URI.create("/save")) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestBody))) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andExpect(MockMvcResultMatchers.content().json(objectMapper.writeValueAsString(expectedResponseBody))); + } +} diff --git a/spring-boot-modules/spring-boot-mvc-5/src/test/java/com/baeldung/modifyrequest/EscapeHtmlFilterIntegrationTest.java b/spring-boot-modules/spring-boot-mvc-5/src/test/java/com/baeldung/modifyrequest/EscapeHtmlFilterIntegrationTest.java new file mode 100644 index 0000000000..35254eb151 --- /dev/null +++ b/spring-boot-modules/spring-boot-mvc-5/src/test/java/com/baeldung/modifyrequest/EscapeHtmlFilterIntegrationTest.java @@ -0,0 +1,51 @@ +package com.baeldung.modifyrequest; + +import com.baeldung.modifyrequest.controller.UserController; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.net.URI; +import java.util.Map; + +@ExtendWith(SpringExtension.class) +@AutoConfigureMockMvc +@WebMvcTest(UserController.class) +@ActiveProfiles("filterExample") +public class EscapeHtmlFilterIntegrationTest { + Logger logger = LoggerFactory.getLogger(EscapeHtmlFilterIntegrationTest.class); + + @Autowired + private MockMvc mockMvc; + @Test + void givenFilter_whenEscapeHtmlFilter_thenEscapeHtml() throws Exception { + Map requestBody = Map.of( + "name", "James Cameron", + "email", "james@gmail.com" + ); + + Map expectedResponseBody = Map.of( + "name", "James Cameron", + "email", "<script>alert()</script>james@gmail.com" + ); + + ObjectMapper objectMapper = new ObjectMapper(); + + mockMvc.perform(MockMvcRequestBuilders.post(URI.create("/save")) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestBody))) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andExpect(MockMvcResultMatchers.content().json(objectMapper.writeValueAsString(expectedResponseBody))); + } +} diff --git a/spring-boot-modules/spring-boot-mvc-5/src/test/java/com/baeldung/modifyrequest/EscapeHtmlInterceptorIntegrationTest.java b/spring-boot-modules/spring-boot-mvc-5/src/test/java/com/baeldung/modifyrequest/EscapeHtmlInterceptorIntegrationTest.java new file mode 100644 index 0000000000..002481de4f --- /dev/null +++ b/spring-boot-modules/spring-boot-mvc-5/src/test/java/com/baeldung/modifyrequest/EscapeHtmlInterceptorIntegrationTest.java @@ -0,0 +1,46 @@ +package com.baeldung.modifyrequest; + +import com.baeldung.modifyrequest.controller.UserController; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.net.URI; +import java.util.Map; + + +@ExtendWith(SpringExtension.class) +@AutoConfigureMockMvc +@WebMvcTest(UserController.class) +@ActiveProfiles("interceptorExample") +public class EscapeHtmlInterceptorIntegrationTest { + Logger logger = LoggerFactory.getLogger(EscapeHtmlInterceptorIntegrationTest.class); + + @Autowired + private MockMvc mockMvc; + + @Test + void givenInterceptor_whenEscapeHtmlInterceptor_thenEscapeHtml() throws Exception { + Map requestBody = Map.of( + "name", "James Cameron", + "email", "james@gmail.com" + ); + + ObjectMapper objectMapper = new ObjectMapper(); + mockMvc.perform(MockMvcRequestBuilders.post(URI.create("/save")) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestBody))) + .andExpect(MockMvcResultMatchers.status().is4xxClientError()); + } +}