Skip to content
Open
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-15 - ์ •์ฑ… ์˜ค๋ฒ„๋ผ์ด๋“œ ์Šน์ธ์ž ID PII ๋กœ๊น… ์ทจ์•ฝ์ 
**Vulnerability:** ์ฐจ๋‹จ๋œ ํ˜•์‹์˜ ์ •์ฑ… ์˜ค๋ฒ„๋ผ์ด๋“œ๊ฐ€ ์ˆ˜๋ฝ๋˜์—ˆ์„ ๋•Œ, ๋ฌธ์„œ ๊ฒ€์ฆ ์„œ๋น„์Šค๊ฐ€ ํ‰๋ฌธ ํ˜•ํƒœ์˜ `approverId`๋ฅผ ๋กœ๊น…ํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฐ’์€ ๋ฏผ๊ฐํ•œ ์‹๋ณ„ ์ •๋ณด(PII)๋ฅผ ํฌํ•จํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์ด๋ฅผ ํ‰๋ฌธ์œผ๋กœ ๊ธฐ๋กํ•˜๋Š” ๊ฒƒ์€ PII ๋กœ๊น… ์ •์ฑ… ์œ„๋ฐ˜์ž…๋‹ˆ๋‹ค.
**Learning:** ๋กœ๊ทธ ์‚ฝ์ž… ๋“ฑ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋ณด์•ˆ ํ† ํฐ์ด๋‚˜ ์‹๋ณ„์ž๋ฅผ ์ •๋ฆฌ(sanitize)ํ•˜๋”๋ผ๋„, ํ•ด๋‹น ๊ฐ’์ด PII๋กœ ๊ฐ„์ฃผ๋˜๋Š” ๊ฒฝ์šฐ ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ค‘์•™ ๋กœ๊น… ์‹œ์Šคํ…œ์— ์œ ์ถœ๋˜๋Š” ๊ฒƒ์„ ๋ง‰๊ธฐ ์œ„ํ•ด ๋ฐ˜๋“œ์‹œ ํ•ด์‹œ ์ฒ˜๋ฆฌ ๋˜๋Š” ์ง€๋ฌธํ™”(fingerprinting)๋ฅผ ์ ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
**Prevention:** PII ๋กœ๊น… ์ •์ฑ…์„ ์ค€์ˆ˜ํ•˜๋ ค๋ฉด ์ •์ฑ… ์˜ค๋ฒ„๋ผ์ด๋“œ์˜ `approverId`์™€ ๊ฐ™์€ ๋ฏผ๊ฐํ•œ ์‹๋ณ„์ž๋ฅผ ํ‰๋ฌธ์œผ๋กœ ๋กœ๊น…ํ•ด์„œ๋Š” ์•ˆ ๋ฉ๋‹ˆ๋‹ค. ๊ฐ์‚ฌ ๋กœ๊ทธ๋ฅผ ๊ธฐ๋กํ•˜๊ธฐ ์ „์— ๋ฐ˜๋“œ์‹œ ๋„(null) ์•ˆ์ •์„ฑ์„ ํฌํ•จํ•œ ํ•ด์‹ฑ ๋˜๋Š” ์ง€๋ฌธํ™”(์˜ˆ: SHA-256 ํ•ด์‹œ ๋ฐ 16์ง„์ˆ˜ ์ธ์ฝ”๋”ฉ)๋ฅผ ์ ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
6 changes: 3 additions & 3 deletions commit_message.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
๐Ÿ›ก๏ธ Sentinel: [CRITICAL] ํŒŒ์ผ ์—…๋กœ๋“œ ๊ฒฝ๋กœ ์กฐ์ž‘(Path Traversal) ์ทจ์•ฝ์  ์ˆ˜์ •
๐Ÿ›ก๏ธ Sentinel: [CRITICAL] PII ๋กœ๊ทธ ์œ ์ถœ ์ทจ์•ฝ์  ์ˆ˜์ • (approverId ํ•ด์‹ฑ)

MultipartFile.getOriginalFilename()์„ ์‹ ๋ขฐํ•˜์—ฌ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒฝ๋กœ ์กฐ์ž‘ ์ทจ์•ฝ์ ์„ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.
StringUtils.cleanPath()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฒฝ๋กœ๋ฅผ ์ •๊ทœํ™”ํ•˜๊ณ  ์ˆœ์ˆ˜ํ•œ ํŒŒ์ผ๋ช…๋งŒ ์ถ”์ถœํ•˜์—ฌ ์•…์˜์ ์ธ ํŽ˜์ด๋กœ๋“œ(์˜ˆ: ../../../etc/passwd.hwp)๋กœ๋ถ€ํ„ฐ ์‹œ์Šคํ…œ์„ ๋ณดํ˜ธํ•ฉ๋‹ˆ๋‹ค.
๋ฌธ์„œ ๊ฒ€์ฆ ์„œ๋น„์Šค์—์„œ ์ •์ฑ… ์˜ค๋ฒ„๋ผ์ด๋“œ ์ˆ˜๋ฝ ์‹œ `approverId`๊ฐ€ ํ‰๋ฌธ์œผ๋กœ ๋กœ๊ทธ์— ๊ธฐ๋ก๋˜์–ด PII(๊ฐœ์ธ์‹๋ณ„์ •๋ณด)๊ฐ€ ์œ ์ถœ๋  ์œ„ํ—˜์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
`approverId`๋ฅผ ๊ธฐ๋กํ•˜๊ธฐ ์ „์— SHA-256 ํ•ด์‹ฑ ๋ฐ 16์ง„์ˆ˜ ์ธ์ฝ”๋”ฉ์„ ์ˆ˜ํ–‰ํ•˜๋Š” `fingerprintApproverId` ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์ด๋ฅผ ํ•ด๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค.
8 changes: 4 additions & 4 deletions description.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
๐Ÿšจ Severity: CRITICAL
๐Ÿ’ก Vulnerability: ํŒŒ์ผ ์—…๋กœ๋“œ ์‹œ `MultipartFile.getOriginalFilename()` ๊ฐ’์„ ๊ฒ€์ฆ ์—†์ด ์‚ฌ์šฉํ•˜์—ฌ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒฝ๋กœ ์กฐ์ž‘(Path Traversal) ์ทจ์•ฝ์  ๋ฐœ๊ฒฌ.
๐ŸŽฏ Impact: ๊ณต๊ฒฉ์ž๊ฐ€ ๋””๋ ‰ํ† ๋ฆฌ ํƒ์ƒ‰ ๋ฌธ์ž์—ด(`../`)์„ ํฌํ•จํ•œ ํŒŒ์ผ๋ช…์„ ์ „์†กํ•˜์—ฌ ์˜๋„ํ•˜์ง€ ์•Š์€ ๊ฒฝ๋กœ์— ํŒŒ์ผ์„ ์ €์žฅํ•˜๊ฑฐ๋‚˜ ์‹œ์Šคํ…œ ํŒŒ์ผ(์˜ˆ: `/etc/passwd`)์— ์ ‘๊ทผ/์กฐ์ž‘ํ•  ์œ„ํ—˜์ด ์žˆ์Œ.
๐Ÿ”ง Fix: `DefaultDocumentConversionService` ๋ฐ `DefaultDocumentValidationService`์—์„œ ํŒŒ์ผ๋ช…์„ ์‚ฌ์šฉํ•˜๊ธฐ ์ „ `StringUtils.cleanPath()`๋ฅผ ํ†ตํ•ด ๊ฒฝ๋กœ๋ฅผ ์ •๊ทœํ™”ํ•˜๊ณ , ๋งˆ์ง€๋ง‰ `/` ์ดํ›„์˜ ์ˆœ์ˆ˜ํ•œ ํŒŒ์ผ๋ช…๋งŒ ์ถ”์ถœํ•˜๋„๋ก `sanitizeFilename` ๋ฉ”์†Œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•จ.
โœ… Verification: ๋‹จ์œ„ ํ…Œ์ŠคํŠธ(`submitStripsDirectoryTraversalFromOriginalFilename` ๋ฐ `stripsDirectoryTraversalFromFilename` ๋“ฑ)๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์ทจ์•ฝ์  ๋ฌธ์ž์—ด์ด ์ •์ƒ์ ์œผ๋กœ ์ œ๊ฑฐ๋˜๋ฉฐ 100% ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ๋ณด์žฅํ•จ.
๐Ÿ’ก Vulnerability: ๋ฌธ์„œ ๊ฒ€์ฆ ์„œ๋น„์Šค์—์„œ ์ •์ฑ… ์˜ค๋ฒ„๋ผ์ด๋“œ ์ˆ˜๋ฝ ์‹œ `approverId`๊ฐ€ ํ‰๋ฌธ์œผ๋กœ ๋กœ๊ทธ์— ๊ธฐ๋ก๋˜์–ด PII(๊ฐœ์ธ์‹๋ณ„์ •๋ณด)๊ฐ€ ์œ ์ถœ๋  ์œ„ํ—˜์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
๐ŸŽฏ Impact: ๊ณต๊ฒฉ์ž๋‚˜ ๋‚ด๋ถ€ ์ง์›์ด ๋กœ๊ทธ ์‹œ์Šคํ…œ์„ ํ†ตํ•ด ๋ฏผ๊ฐํ•œ ์‹๋ณ„ ์ •๋ณด๋ฅผ ์—ด๋žŒํ•˜์—ฌ ๊ฐœ์ธ์ •๋ณด ์นจํ•ด๋‚˜ ๊ถŒํ•œ ๋„์šฉ์œผ๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
๐Ÿ”ง Fix: `approverId`๋ฅผ ๊ธฐ๋กํ•˜๊ธฐ ์ „์— ๋„(null) ์•ˆ์ •์„ฑ์ด ํ™•๋ณด๋œ SHA-256 ํ•ด์‹ฑ ๋ฐ 16์ง„์ˆ˜ ์ธ์ฝ”๋”ฉ์„ ์ˆ˜ํ–‰ํ•˜๋Š” `fingerprintApproverId` ๋ฉ”์„œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ์•ˆ์ „ํ•˜๊ฒŒ ์ง€๋ฌธํ™”(fingerprinting)ํ•˜๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ฐ€์ด๋“œ๋ผ์ธ์— ๋”ฐ๋ผ ๊ธฐ์กด์˜ ๋‹จ์ผ `HEX_FORMAT` ์ƒ์ˆ˜๋ฅผ ์žฌ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋„ 100% ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ๋งŒ์กฑํ•˜๋„๋ก ์ถ”๊ฐ€ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
โœ… Verification: `mvn test`๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ์ƒˆ๋กœ ์ถ”๊ฐ€๋œ `fingerprintApproverIdReturnsEmptyWhenInputIsNull` ๋ฐ `throwsWhenSha256DigestIsUnavailableForApproverIdFingerprint` ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผํ•˜๋Š”์ง€ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.
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),
fingerprintApproverId(overrideApproverIdForAudit),
tokenFingerprint(overrideTokenForAudit)
);
}
Expand Down Expand Up @@ -209,6 +209,20 @@ private String tokenFingerprint(String approvalToken) {
}
}

private String fingerprintApproverId(String approverId) {
if (approverId == null || approverId.isBlank()) {
return "";
}
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashed = digest.digest(approverId.getBytes(StandardCharsets.UTF_8));
// Reused HexFormat for performance
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 @@ -525,6 +525,46 @@ void sanitizeForLogReplacesTabCharacter() throws Exception {
assertEquals("approver_id", sanitized);
}

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

String result = (String) method.invoke(validationService, new Object[] {null});

assertEquals("", result);
}

@Test
void throwsWhenSha256DigestIsUnavailableForApproverIdFingerprint() throws Exception {
ConversionProperties conversionProperties = new ConversionProperties();
DefaultDocumentValidationService validationService = new DefaultDocumentValidationService(conversionProperties);
Method method = DefaultDocumentValidationService.class.getDeclaredMethod("fingerprintApproverId", 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("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