Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,8 @@
**Vulnerability:** The document hashing routine in `DefaultDocumentConversionService` processed file streams without enforcing any maximum size limit on the bytes read. An attacker could exploit this by uploading a maliciously large stream (or exploiting a compression bomb if unzipping), exhausting system memory, CPU, or disk space (DoS).
**Learning:** Checking the declared file size (e.g., `file.getSize()`) in initial validation is not always sufficient if the input stream itself can be spoofed or dynamically expanded during reading. The actual bytes read must be verified against bounds continuously.
**Prevention:** Always enforce a strict, configurable size limit (e.g., `ConversionProperties.maxUploadSizeBytes`) within the `while` loop that reads from untrusted input streams. Track `totalRead` and throw an exception immediately if the limit is exceeded.

## 2026-07-26 - [PII exposure in audit logging]
**Vulnerability:** The `approverId` in the `PolicyOverrideRequest` was being logged in plaintext when a blocked file extension was allowed.
**Learning:** System audit logs were capturing sensitive user identifiers without pseudonimization, violating PII rules.
**Prevention:** Always hash or fingerprint sensitive identifiers (using a secure hashing algorithm like SHA-256) before appending them to audit log streams.
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public void validateOrThrow(MultipartFile file, PolicyOverrideRequest overrideRe
LOGGER.info(
"Blocked-format override accepted extension={} approverId={} tokenFingerprint={}",
sanitizeForLog(extension),
sanitizeForLog(overrideApproverIdForAudit),
hashApproverId(overrideApproverIdForAudit),
tokenFingerprint(overrideTokenForAudit)
);
Comment on lines 115 to 120
}
Expand Down Expand Up @@ -209,6 +209,19 @@ private String tokenFingerprint(String approvalToken) {
}
}

private String hashApproverId(String approverId) {
if (approverId == null) {
return "null";
}
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashed = digest.digest(approverId.getBytes(StandardCharsets.UTF_8));
return HEX_FORMAT.formatHex(hashed);
} catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException("SHA-256 digest unavailable", ex);
}
}

private String sanitizeForLog(final String value) {
if (value == null) {
return "";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,16 @@ void sanitizeForLogReturnsEmptyWhenInputIsNull() throws Exception {
assertEquals("", sanitized);
}

@Test
void hashApproverIdReturnsNullStringWhenInputIsNull() throws Exception {
ConversionProperties conversionProperties = new ConversionProperties();
DefaultDocumentValidationService validationService = new DefaultDocumentValidationService(conversionProperties);
Method method = DefaultDocumentValidationService.class.getDeclaredMethod("hashApproverId", String.class);
method.setAccessible(true);
String hashed = (String) method.invoke(validationService, (String) null);
assertEquals("null", hashed);
}
Comment on lines +516 to +524

@Test
void sanitizeForLogReplacesTabCharacter() throws Exception {
ConversionProperties conversionProperties = new ConversionProperties();
Expand All @@ -525,6 +535,33 @@ void sanitizeForLogReplacesTabCharacter() throws Exception {
assertEquals("approver_id", sanitized);
}

@Test
void throwsWhenSha256DigestIsUnavailableForHashApproverId() throws Exception {
ConversionProperties conversionProperties = new ConversionProperties();
DefaultDocumentValidationService validationService = new DefaultDocumentValidationService(conversionProperties);
Method method = DefaultDocumentValidationService.class.getDeclaredMethod("hashApproverId", String.class);
method.setAccessible(true);

synchronized (SECURITY_PROVIDERS_LOCK) {
Provider[] providers = Security.getProviders();
for (Provider provider : providers) {
Security.removeProvider(provider.getName());
}
try {
java.lang.reflect.InvocationTargetException ex = assertThrows(
java.lang.reflect.InvocationTargetException.class,
() -> method.invoke(validationService, "approver-1")
);
assertEquals(IllegalStateException.class, ex.getCause().getClass());
assertEquals("SHA-256 digest unavailable", ex.getCause().getMessage());
} finally {
for (int index = 0; index < providers.length; index++) {
Security.insertProviderAt(providers[index], index + 1);
}
}
}
}

@Test
void throwsWhenSha256DigestIsUnavailableForOverrideAuditFingerprint() {
ConversionProperties conversionProperties = new ConversionProperties();
Expand Down
Loading