diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 76a974257..c3f335050 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -2,3 +2,8 @@ **Vulnerability:** Untrusted paths from API responses were directly assigned to `a.href` and used in `iframe` generation, which allows execution of malicious URIs like `javascript:` or `data:`. **Learning:** Even when avoiding `innerHTML`, directly setting URL-like strings to DOM attributes without protocol validation introduces XSS vectors. The payload can be executed when the link is clicked or the iframe is loaded. **Prevention:** Implement an `isSafeUrl` verification function to ensure the protocol is strictly `http:` or `https:` (using `new URL()`) before assigning untrusted inputs to DOM attributes like `href` or `src`. + +## 2026-07-08 - Prevent Null Byte Injection in File Validation +**Vulnerability:** The document validation service relied on the original filename without verifying the presence of null bytes (`\u0000`). This allows attackers to bypass file extension checks via truncation (e.g., `malicious.hwp\u0000.pdf`). +**Learning:** Checking the file extension using standard string operations can be bypassed if the underlying system API (like a C-based filesystem or a library handling byte arrays) truncates the string at the first null byte. +**Prevention:** Explicitly check for the presence of null bytes (`\u0000`) in the filename as early as possible and throw an exception to reject the input entirely, rather than attempting to sanitize it. diff --git a/pom.xml b/pom.xml index 6b7e40df2..6a8f6d9b0 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,31 @@ 4.10.38 + + + + org.bouncycastle + bcprov-jdk18on + 1.84 + + + org.bouncycastle + bcpkix-jdk18on + 1.84 + + + org.bouncycastle + bcutil-jdk18on + 1.84 + + + org.bouncycastle + bcjmail-jdk18on + 1.84 + + + + org.springframework.boot diff --git a/src/main/java/com/clearfolio/viewer/service/DefaultDocumentValidationService.java b/src/main/java/com/clearfolio/viewer/service/DefaultDocumentValidationService.java index b898f7ac8..a7c7e6c0b 100644 --- a/src/main/java/com/clearfolio/viewer/service/DefaultDocumentValidationService.java +++ b/src/main/java/com/clearfolio/viewer/service/DefaultDocumentValidationService.java @@ -49,12 +49,18 @@ public void validateOrThrow(MultipartFile file) { * {@inheritDoc} */ @Override - public void validateOrThrow(MultipartFile file, PolicyOverrideRequest overrideRequest) { + public void validateOrThrow(final MultipartFile file, final PolicyOverrideRequest overrideRequest) { if (file == null || file.isEmpty()) { throw new IllegalArgumentException("File is required."); } String fileName = file.getOriginalFilename(); + if (fileName != null && fileName.indexOf('\u0000') >= 0) { + throw new IllegalArgumentException( + "File name contains invalid characters." + ); + } + String extension = extensionOf(fileName); if (extension.isEmpty()) { throw new IllegalArgumentException("File extension is required."); diff --git a/src/test/java/com/clearfolio/viewer/service/DefaultDocumentValidationServiceTest.java b/src/test/java/com/clearfolio/viewer/service/DefaultDocumentValidationServiceTest.java index dc1df7934..d0eb7c4ad 100644 --- a/src/test/java/com/clearfolio/viewer/service/DefaultDocumentValidationServiceTest.java +++ b/src/test/java/com/clearfolio/viewer/service/DefaultDocumentValidationServiceTest.java @@ -283,6 +283,22 @@ void rejectsMultipartWithNullOriginalFilename() { assertEquals("File extension is required.", ex.getMessage()); } + @Test + void rejectsFilenameWithNullByte() { + ConversionProperties conversionProperties = new ConversionProperties(); + conversionProperties.setBlockedExtensions(Set.of("hwp", "hwpx")); + DefaultDocumentValidationService validationService = new DefaultDocumentValidationService(conversionProperties); + + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> validationService.validateOrThrow( + new MockMultipartFile("file", "contract.hwp\u0000.pdf", "application/octet-stream", new byte[] {1}) + ) + ); + + assertEquals("File name contains invalid characters.", ex.getMessage()); + } + @Test void rejectsFilenameEndingWithDot() { ConversionProperties conversionProperties = new ConversionProperties();