diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 76a97425..251c2fb5 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -2,3 +2,7 @@ **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-06 - Prevent Truncation Bypass Attacks via Null Bytes +**Vulnerability:** File validation logic (`DefaultDocumentValidationService.java`) failed to properly check for null bytes (`\u0000`) in filenames, relying on a naive replace/sanitize step in a different method that didn't prevent an attacker from bypassing the extension check. +**Learning:** Never silently sanitize or truncate input when performing security validations like file extensions. Attackers can use null bytes (`.hwp\0.pdf`) to trick the extension check (`.pdf`) while the underlying OS or downstream services process the truncated file (`.hwp`). +**Prevention:** Explicitly reject any input containing a null byte by throwing an exception immediately. Do not attempt to strip or sanitize it. diff --git a/src/main/java/com/clearfolio/viewer/service/DefaultDocumentValidationService.java b/src/main/java/com/clearfolio/viewer/service/DefaultDocumentValidationService.java index b898f7ac..61a704a6 100644 --- a/src/main/java/com/clearfolio/viewer/service/DefaultDocumentValidationService.java +++ b/src/main/java/com/clearfolio/viewer/service/DefaultDocumentValidationService.java @@ -55,6 +55,12 @@ public void validateOrThrow(MultipartFile file, PolicyOverrideRequest overrideRe } String fileName = file.getOriginalFilename(); + if (fileName != null && fileName.indexOf('\0') != -1) { + throw new IllegalArgumentException( + "Filename cannot contain null bytes." + ); + } + 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 dc1df793..1410964e 100644 --- a/src/test/java/com/clearfolio/viewer/service/DefaultDocumentValidationServiceTest.java +++ b/src/test/java/com/clearfolio/viewer/service/DefaultDocumentValidationServiceTest.java @@ -401,4 +401,20 @@ void throwsWhenSha256DigestIsUnavailableForOverrideAuditFingerprint() { } } } + + @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\0.pdf", "application/octet-stream", new byte[] {1}) + ) + ); + + assertEquals("Filename cannot contain null bytes.", ex.getMessage()); + } }