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 eaf992f71..9667348d6 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 @@ -1,41 +1,44 @@ 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 static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.*; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; 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.model.ApplicationProperties; import stirling.software.common.service.SsrfProtectionService; class CustomHtmlSanitizerTest { + // Changed: Promote mocks to fields so we can re-stub behavior per test where necessary. + private SsrfProtectionService ssrfProtectionService; + private ApplicationProperties applicationProperties; + private ApplicationProperties.System systemProperties; + 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); + ssrfProtectionService = mock(SsrfProtectionService.class); + applicationProperties = mock(ApplicationProperties.class); + systemProperties = mock(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 + // Default behavior: allow all URLs and enable sanitization + when(ssrfProtectionService.isUrlAllowed(anyString())).thenReturn(true); + when(applicationProperties.getSystem()).thenReturn(systemProperties); + when(systemProperties.getDisableSanitize()).thenReturn(false); - customHtmlSanitizer = - new CustomHtmlSanitizer(mockSsrfProtectionService, mockApplicationProperties); + customHtmlSanitizer = new CustomHtmlSanitizer(ssrfProtectionService, applicationProperties); } @ParameterizedTest @@ -56,10 +59,11 @@ class CustomHtmlSanitizerTest { "

This is valid HTML with formatting.

", new String[] {"

", "", ""}), Arguments.of( - "

Text with bold, italic, underline, " - + "emphasis, strong, strikethrough, " - + "strike, subscript, superscript, " - + "teletype, code, big, small.

", + "

Text with bold, italic, underline," + + " emphasis, strong," + + " strikethrough, strike," + + " subscript, superscript, teletype," + + " code, big, small.

", new String[] { "bold", "italic", @@ -163,7 +167,7 @@ class CustomHtmlSanitizerTest { @Test void testSanitizeAllowsImages() { - // Arrange - Testing Sanitizers.IMAGES + // Arrange - Testing custom images policy with SSRF check String htmlWithImage = "\"An"; @@ -182,7 +186,13 @@ class CustomHtmlSanitizerTest { void testSanitizeDisallowsDataUrlImages() { // Arrange String htmlWithDataUrlImage = - "\"SVG"; + "\"SVG"; + + // Changed: Explicitly tell SSRF service to reject data: URLs so the custom AttributePolicy + // drops the src attribute. Without this, a permissive SSRF mock might allow data: URLs. + when(ssrfProtectionService.isUrlAllowed(argThat(v -> v != null && v.startsWith("data:")))) + .thenReturn(false); // Act String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithDataUrlImage); @@ -257,9 +267,9 @@ class CustomHtmlSanitizerTest { void testSanitizeRemovesObjectAndEmbed() { // Arrange String htmlWithObjects = - "

Safe content

" - + "" - + ""; + "

Safe content

"; // Act String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithObjects); @@ -295,17 +305,12 @@ class CustomHtmlSanitizerTest { void testSanitizeHandlesComplexHtml() { // Arrange String complexHtml = - "
" - + "

Welcome

" - + "

This is a test with link.

" - + " " - + " " - + " " - + "
NameValue
Item 1100
" - + " \"Test" - + " " - + " " - + "
"; + "

Welcome

This is a" + + " test with link.

" + + "
NameValue
Item" + + " 1100
\"Test"
"; // Act String sanitizedHtml = customHtmlSanitizer.sanitize(complexHtml); @@ -353,4 +358,117 @@ class CustomHtmlSanitizerTest { // Assert assertEquals("", sanitizedHtml, "Null input should result in empty string"); } + + // ----------------------- + // Additional coverage + // ----------------------- + + @Test + @DisplayName("Should return input unchanged when sanitize is disabled via properties") + void shouldBypassSanitizationWhenDisabled() { + // Arrange + String malicious = + "

ok

"; + + // For this test, disable sanitize + when(systemProperties.getDisableSanitize()).thenReturn(true); + + // Also ensure SSRF would block it if sanitization were enabled (to prove bypass) + when(ssrfProtectionService.isUrlAllowed(anyString())).thenReturn(false); + + // Act + String result = customHtmlSanitizer.sanitize(malicious); + + // Assert + // When disabled, sanitizer must return original string as-is. + assertEquals(malicious, result, "Sanitization disabled should return input as-is"); + } + + @Test + @DisplayName("Should remove img src when SSRF service rejects the URL") + void shouldRemoveImageSrcWhenSsrfRejects() { + // Arrange + String html = "\"x\""; + + // Reject this URL + when(ssrfProtectionService.isUrlAllowed("http://internal/admin")).thenReturn(false); + + // Act + String sanitized = customHtmlSanitizer.sanitize(html); + + // Assert + assertTrue(sanitized.contains(""; + + // Explicit allow (clarity) + when(ssrfProtectionService.isUrlAllowed("https://example.com/image.jpg")).thenReturn(true); + + // Act + String sanitized = customHtmlSanitizer.sanitize(html); + + // Assert + assertTrue(sanitized.contains(""; + + // Act + String sanitized = customHtmlSanitizer.sanitize(html); + + // Assert + assertTrue(sanitized.contains("mail me"; + String sanitized = customHtmlSanitizer.sanitize(html); + + // Anchor and text should remain + assertTrue(sanitized.contains(""), + "Sanitized output should keep anchor; mailto: may or may not be present" + + " depending on sanitizer configuration"); + } + } }