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();