diff --git a/app/common/src/main/java/stirling/software/common/util/CustomHtmlSanitizer.java b/app/common/src/main/java/stirling/software/common/util/CustomHtmlSanitizer.java index 05542c3ea..05d9b73a6 100644 --- a/app/common/src/main/java/stirling/software/common/util/CustomHtmlSanitizer.java +++ b/app/common/src/main/java/stirling/software/common/util/CustomHtmlSanitizer.java @@ -7,16 +7,21 @@ import org.owasp.html.Sanitizers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.SsrfProtectionService; @Component public class CustomHtmlSanitizer { private final SsrfProtectionService ssrfProtectionService; + private final ApplicationProperties applicationProperties; @Autowired - public CustomHtmlSanitizer(SsrfProtectionService ssrfProtectionService) { + public CustomHtmlSanitizer( + SsrfProtectionService ssrfProtectionService, + ApplicationProperties applicationProperties) { this.ssrfProtectionService = ssrfProtectionService; + this.applicationProperties = applicationProperties; } private final AttributePolicy SSRF_SAFE_URL_POLICY = @@ -39,7 +44,7 @@ public class CustomHtmlSanitizer { } }; - private static final PolicyFactory SSRF_SAFE_IMAGES_POLICY = + private final PolicyFactory SSRF_SAFE_IMAGES_POLICY = new HtmlPolicyBuilder() .allowElements("img") .allowAttributes("alt", "width", "height", "title") @@ -49,7 +54,7 @@ public class CustomHtmlSanitizer { .onElements("img") .toFactory(); - private static final PolicyFactory POLICY = + private final PolicyFactory POLICY = Sanitizers.FORMATTING .and(Sanitizers.BLOCKS) .and(Sanitizers.STYLES) @@ -58,7 +63,9 @@ public class CustomHtmlSanitizer { .and(SSRF_SAFE_IMAGES_POLICY) .and(new HtmlPolicyBuilder().disallowElements("noscript").toFactory()); - public static String sanitize(String html) { - return POLICY.sanitize(html); + public String sanitize(String html) { + boolean disableSanitize = + Boolean.TRUE.equals(applicationProperties.getSystem().getDisableSanitize()); + return disableSanitize ? html : POLICY.sanitize(html); } } diff --git a/app/common/src/main/java/stirling/software/common/util/EmlToPdf.java b/app/common/src/main/java/stirling/software/common/util/EmlToPdf.java index 05e9cec5c..6b28dc683 100644 --- a/app/common/src/main/java/stirling/software/common/util/EmlToPdf.java +++ b/app/common/src/main/java/stirling/software/common/util/EmlToPdf.java @@ -133,9 +133,9 @@ public class EmlToPdf { EmlToPdfRequest request, byte[] emlBytes, String fileName, - boolean disableSanitize, stirling.software.common.service.CustomPDFDocumentFactory pdfDocumentFactory, - TempFileManager tempFileManager) + TempFileManager tempFileManager, + CustomHtmlSanitizer customHtmlSanitizer) throws IOException, InterruptedException { validateEmlInput(emlBytes); @@ -155,7 +155,11 @@ public class EmlToPdf { // Convert HTML to PDF byte[] pdfBytes = convertHtmlToPdf( - weasyprintPath, request, htmlContent, disableSanitize, tempFileManager); + weasyprintPath, + request, + htmlContent, + tempFileManager, + customHtmlSanitizer); // Attach files if available and requested if (shouldAttachFiles(emailContent, request)) { @@ -196,8 +200,8 @@ public class EmlToPdf { String weasyprintPath, EmlToPdfRequest request, String htmlContent, - boolean disableSanitize, - TempFileManager tempFileManager) + TempFileManager tempFileManager, + CustomHtmlSanitizer customHtmlSanitizer) throws IOException, InterruptedException { HTMLToPdfRequest htmlRequest = createHtmlRequest(request); @@ -208,8 +212,8 @@ public class EmlToPdf { htmlRequest, htmlContent.getBytes(StandardCharsets.UTF_8), "email.html", - disableSanitize, - tempFileManager); + tempFileManager, + customHtmlSanitizer); } catch (IOException | InterruptedException e) { log.warn("Initial HTML to PDF conversion failed, trying with simplified HTML"); String simplifiedHtml = simplifyHtmlContent(htmlContent); @@ -218,8 +222,8 @@ public class EmlToPdf { htmlRequest, simplifiedHtml.getBytes(StandardCharsets.UTF_8), "email.html", - disableSanitize, - tempFileManager); + tempFileManager, + customHtmlSanitizer); } } 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 c735e5287..799f91e05 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 @@ -26,8 +26,8 @@ public class FileToPdf { HTMLToPdfRequest request, byte[] fileBytes, String fileName, - boolean disableSanitize, - TempFileManager tempFileManager) + TempFileManager tempFileManager, + CustomHtmlSanitizer customHtmlSanitizer) throws IOException, InterruptedException { try (TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf")) { @@ -39,14 +39,15 @@ public class FileToPdf { if (fileName.toLowerCase().endsWith(".html")) { String sanitizedHtml = sanitizeHtmlContent( - new String(fileBytes, StandardCharsets.UTF_8), disableSanitize); + new String(fileBytes, StandardCharsets.UTF_8), + customHtmlSanitizer); Files.write( tempInputFile.getPath(), sanitizedHtml.getBytes(StandardCharsets.UTF_8)); } else if (fileName.toLowerCase().endsWith(".zip")) { Files.write(tempInputFile.getPath(), fileBytes); sanitizeHtmlFilesInZip( - tempInputFile.getPath(), disableSanitize, tempFileManager); + tempInputFile.getPath(), tempFileManager, customHtmlSanitizer); } else { throw ExceptionUtils.createHtmlFileRequiredException(); } @@ -78,12 +79,15 @@ public class FileToPdf { } // tempOutputFile auto-closed } - private static String sanitizeHtmlContent(String htmlContent, boolean disableSanitize) { - return (!disableSanitize) ? CustomHtmlSanitizer.sanitize(htmlContent) : htmlContent; + private static String sanitizeHtmlContent( + String htmlContent, CustomHtmlSanitizer customHtmlSanitizer) { + return customHtmlSanitizer.sanitize(htmlContent); } private static void sanitizeHtmlFilesInZip( - Path zipFilePath, boolean disableSanitize, TempFileManager tempFileManager) + Path zipFilePath, + TempFileManager tempFileManager, + CustomHtmlSanitizer customHtmlSanitizer) throws IOException { try (TempDirectory tempUnzippedDir = new TempDirectory(tempFileManager)) { try (ZipInputStream zipIn = @@ -99,7 +103,8 @@ public class FileToPdf { || entry.getName().toLowerCase().endsWith(".htm")) { String content = new String(zipIn.readAllBytes(), StandardCharsets.UTF_8); - String sanitizedContent = sanitizeHtmlContent(content, disableSanitize); + String sanitizedContent = + sanitizeHtmlContent(content, customHtmlSanitizer); Files.write( filePath, sanitizedContent.getBytes(StandardCharsets.UTF_8)); } else { diff --git a/app/common/src/test/java/stirling/software/common/util/CustomHtmlSanitizerTest.java b/app/common/src/test/java/stirling/software/common/util/CustomHtmlSanitizerTest.java index 65bffe05e..59e5f81b1 100644 --- a/app/common/src/test/java/stirling/software/common/util/CustomHtmlSanitizerTest.java +++ b/app/common/src/test/java/stirling/software/common/util/CustomHtmlSanitizerTest.java @@ -3,21 +3,42 @@ package stirling.software.common.util; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import stirling.software.common.service.SsrfProtectionService; + class CustomHtmlSanitizerTest { + private CustomHtmlSanitizer customHtmlSanitizer; + + @BeforeEach + void setUp() { + SsrfProtectionService mockSsrfProtectionService = mock(SsrfProtectionService.class); + stirling.software.common.model.ApplicationProperties mockApplicationProperties = mock(stirling.software.common.model.ApplicationProperties.class); + stirling.software.common.model.ApplicationProperties.System mockSystem = mock(stirling.software.common.model.ApplicationProperties.System.class); + + // Allow all URLs by default for basic tests + when(mockSsrfProtectionService.isUrlAllowed(org.mockito.ArgumentMatchers.anyString())).thenReturn(true); + when(mockApplicationProperties.getSystem()).thenReturn(mockSystem); + when(mockSystem.getDisableSanitize()).thenReturn(false); // Enable sanitization for tests + + customHtmlSanitizer = new CustomHtmlSanitizer(mockSsrfProtectionService, mockApplicationProperties); + } + @ParameterizedTest @MethodSource("provideHtmlTestCases") void testSanitizeHtml(String inputHtml, String[] expectedContainedTags) { // Act - String sanitizedHtml = CustomHtmlSanitizer.sanitize(inputHtml); + String sanitizedHtml = customHtmlSanitizer.sanitize(inputHtml); // Assert for (String tag : expectedContainedTags) { @@ -58,7 +79,7 @@ class CustomHtmlSanitizerTest { "
Styled text
"; // Act - String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithStyles); + String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithStyles); // Assert // The OWASP HTML Sanitizer might filter some specific styles, so we only check that @@ -75,7 +96,7 @@ class CustomHtmlSanitizerTest { "Example Link"; // Act - String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithLink); + String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithLink); // Assert // The most important aspect is that the link content is preserved @@ -97,7 +118,7 @@ class CustomHtmlSanitizerTest { String htmlWithJsLink = "Malicious Link"; // Act - String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithJsLink); + String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithJsLink); // Assert assertFalse(sanitizedHtml.contains("javascript:"), "JavaScript URLs should be removed"); @@ -116,7 +137,7 @@ class CustomHtmlSanitizerTest { + ""; // Act - String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithTable); + String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithTable); // Assert assertTrue(sanitizedHtml.contains("