diff --git a/build.gradle b/build.gradle index 25ba3e0..7d550e7 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ plugins { } group = 'com.flexcodelabs' -version = '0.0.42' +version = '0.0.43' description = 'Flextuma App' java { diff --git a/src/main/java/com/flexcodelabs/flextuma/core/exceptions/GlobalExceptionHandler.java b/src/main/java/com/flexcodelabs/flextuma/core/exceptions/GlobalExceptionHandler.java index 703edef..455fd85 100644 --- a/src/main/java/com/flexcodelabs/flextuma/core/exceptions/GlobalExceptionHandler.java +++ b/src/main/java/com/flexcodelabs/flextuma/core/exceptions/GlobalExceptionHandler.java @@ -168,7 +168,9 @@ public ResponseEntity handleAccessDenied(AccessDeniedException ex) { public ResponseEntity handleNotFound(HttpServletRequest request) { String requestUri = request.getRequestURI(); String method = request.getMethod(); - if ("GET".equals(method) && !requestUri.startsWith("/api/")) { + String accept = request.getHeader("Accept"); + boolean acceptsHtml = accept != null && accept.contains(MediaType.TEXT_HTML_VALUE); + if ("GET".equals(method) && acceptsHtml && !requestUri.startsWith("/api/")) { try { Path filePath = Paths.get(frontendDirectory, "index.html"); Resource resource = new FileSystemResource(filePath.toString()); @@ -360,4 +362,4 @@ private HttpStatus getResponseStatus(String message, HttpStatus defaultStatus) { return HttpStatus.NOT_FOUND; return defaultStatus; } -} \ No newline at end of file +} diff --git a/src/main/java/com/flexcodelabs/flextuma/modules/app/controllers/FrontendController.java b/src/main/java/com/flexcodelabs/flextuma/modules/app/controllers/FrontendController.java index 42c879d..fd21ac2 100644 --- a/src/main/java/com/flexcodelabs/flextuma/modules/app/controllers/FrontendController.java +++ b/src/main/java/com/flexcodelabs/flextuma/modules/app/controllers/FrontendController.java @@ -52,11 +52,19 @@ public ResponseEntity serveCatchAll(HttpServletRequest request) { return serveStatic(path); } - if (path.startsWith(".well-known/")) { + if (path.startsWith(".well-known/") && acceptsHtml(request)) { return serveStatic(indexFile); } - return serveStatic(indexFile); + // Only browser document navigations should receive the SPA shell. Requests + // from vulnerability scanners (for example /wp-login.php) normally accept + // any content type and must remain a 404 instead of appearing to be routes. + return acceptsHtml(request) ? serveStatic(indexFile) : ResponseEntity.notFound().build(); + } + + private boolean acceptsHtml(HttpServletRequest request) { + String accept = request.getHeader("Accept"); + return accept != null && accept.contains(MediaType.TEXT_HTML_VALUE); } private ResponseEntity serveStatic(String path) { @@ -64,7 +72,7 @@ private ResponseEntity serveStatic(String path) { Path filePath = Paths.get(frontendDirectory, path); Resource resource = new FileSystemResource(filePath.toString()); - if (resource.exists() || resource.isReadable()) { + if (resource.exists() && resource.isReadable()) { String contentType = getContentType(path); return ResponseEntity.ok() diff --git a/src/test/java/com/flexcodelabs/flextuma/modules/app/controllers/FrontendControllerTest.java b/src/test/java/com/flexcodelabs/flextuma/modules/app/controllers/FrontendControllerTest.java index 9577cca..5376a8a 100644 --- a/src/test/java/com/flexcodelabs/flextuma/modules/app/controllers/FrontendControllerTest.java +++ b/src/test/java/com/flexcodelabs/flextuma/modules/app/controllers/FrontendControllerTest.java @@ -4,6 +4,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import org.springframework.http.MediaType; + import java.nio.file.Files; import java.nio.file.Path; @@ -33,24 +35,23 @@ void setUp() throws Exception { } @Test - void serveCatchAll_shouldReturnIndexForNonApiRoutes() throws Exception { - mockMvc.perform(get("/dashboard/overview")) + void serveCatchAll_shouldReturnIndexForBrowserNavigation() throws Exception { + mockMvc.perform(get("/dashboard/overview").header("Accept", MediaType.TEXT_HTML_VALUE)) .andExpect(status().isOk()) .andExpect(content().string("app")); } @Test void serveCatchAll_shouldReturnIndexForLoginRoute() throws Exception { - mockMvc.perform(get("/login")) + mockMvc.perform(get("/login").header("Accept", MediaType.TEXT_HTML_VALUE)) .andExpect(status().isOk()) .andExpect(content().string("app")); } @Test - void serveCatchAll_shouldReturnIndexForDottedNonApiRoutes() throws Exception { - mockMvc.perform(get("/foo.bar")) - .andExpect(status().isOk()) - .andExpect(content().string("app")); + void serveCatchAll_shouldReturnNotFoundForNonBrowserRequests() throws Exception { + mockMvc.perform(get("/wp-login.php").header("Accept", "*/*")) + .andExpect(status().isNotFound()); } @Test