diff --git a/app/common/src/main/java/stirling/software/common/util/FileToPdf.java b/app/common/src/main/java/stirling/software/common/util/FileToPdf.java index 6e0fec08e..1b94b7c74 100644 --- a/app/common/src/main/java/stirling/software/common/util/FileToPdf.java +++ b/app/common/src/main/java/stirling/software/common/util/FileToPdf.java @@ -100,6 +100,11 @@ public class FileToPdf { while (entry != null) { Path filePath = tempUnzippedDir.getPath().resolve(sanitizeZipFilename(entry.getName())); + Path normalizedTargetDir = tempUnzippedDir.getPath().toAbsolutePath().normalize(); + Path normalizedFilePath = filePath.toAbsolutePath().normalize(); + if (!normalizedFilePath.startsWith(normalizedTargetDir)) { + throw new IOException("Zip entry path escapes target directory: " + entry.getName()); + } if (!entry.isDirectory()) { Files.createDirectories(filePath.getParent()); if (entry.getName().toLowerCase(Locale.ROOT).endsWith(".html") diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java index d6f6e9054..00f576285 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java @@ -166,7 +166,7 @@ public class ConvertWebsiteToPDF { private String fetchRemoteHtml(String url) throws IOException, InterruptedException { HttpClient client = HttpClient.newBuilder() - .followRedirects(HttpClient.Redirect.NORMAL) + .followRedirects(HttpClient.Redirect.NEVER) .connectTimeout(Duration.ofSeconds(10)) .build(); @@ -180,7 +180,7 @@ public class ConvertWebsiteToPDF { HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); - if (response.statusCode() >= 400 || response.body() == null) { + if (response.statusCode() >= 300 || response.body() == null) { throw ExceptionUtils.createIOException( "error.httpRequestFailed", "Failed to retrieve remote HTML. Status: {0}", diff --git a/app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java b/app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java index 8ef865bd4..6eac7f71e 100644 --- a/app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java +++ b/app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java @@ -29,6 +29,8 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import com.fasterxml.jackson.databind.ObjectMapper; + import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.ExceptionUtils.*; import stirling.software.common.util.RegexPatternUtils; @@ -866,21 +868,18 @@ public class GlobalExceptionHandler { response.setContentType("application/problem+json"); response.setCharacterEncoding("UTF-8"); - String errorJson = - String.format( - """ - { - "type": "about:blank", - "title": "Not Acceptable", - "status": 406, - "detail": "The requested resource could not be returned in an acceptable format. Error responses are returned as JSON.", - "instance": "%s", - "timestamp": "%s", - "hints": ["Error responses are always returned as application/json or application/problem+json", "Set Accept header to include application/json for proper error handling"] - } - """, - request.getRequestURI(), Instant.now().toString()); + // Use ObjectMapper to properly escape JSON values and prevent XSS + ObjectMapper mapper = new ObjectMapper(); + java.util.Map errorMap = new java.util.LinkedHashMap<>(); + errorMap.put("type", "about:blank"); + errorMap.put("title", "Not Acceptable"); + errorMap.put("status", 406); + errorMap.put("detail", "The requested resource could not be returned in an acceptable format. Error responses are returned as JSON."); + errorMap.put("instance", request.getRequestURI()); + errorMap.put("timestamp", Instant.now().toString()); + errorMap.put("hints", java.util.Arrays.asList("Error responses are always returned as application/json or application/problem+json", "Set Accept header to include application/json for proper error handling")); + String errorJson = mapper.writeValueAsString(errorMap); response.getWriter().write(errorJson); response.getWriter().flush(); } diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java index c502449e8..a85e6a666 100644 --- a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java @@ -298,7 +298,7 @@ public class ConvertWebsiteToPdfTest { HttpResponse response = Mockito.mock(); httpClientStatic.when(HttpClient::newBuilder).thenReturn(builder); - when(builder.followRedirects(HttpClient.Redirect.NORMAL)).thenReturn(builder); + when(builder.followRedirects(HttpClient.Redirect.NEVER)).thenReturn(builder); when(builder.connectTimeout(any(Duration.class))).thenReturn(builder); when(builder.build()).thenReturn(client);