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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
- KPI 스냅샷 증거를 다시 불러오는 `refreshKpiEvidence` 동작 중에 "Refresh evidence" 버튼을 비활성화하고 "Refreshing..." 이라는 피드백을 제공하여 사용자의 중복 클릭을 방지했습니다.
- 버튼 상태 변경 시 내부 DOM 구조를 보존하기 위해 `Array.from(button.childNodes)`로 원래 노드를 저장하고, 성공 및 실패 후 `finally` 블록에서 `replaceChildren(...)`으로 안전하게 복원하도록 구현했습니다.

### Fixed
- `DefaultDocumentValidationService`의 `java.nio.file.Path.of` 기반 파일명 추출 로직을 `StringUtils.cleanPath()`로 변경하여 경로 탐색(Path Traversal) 공격 방지 강화
- `tokenFingerprint` 메서드에서 인자가 null일 때 발생하는 `NullPointerException` 방지 로직 추가

## [0.1.0] - 2026-06-25

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,12 @@ private String extensionOf(final String fileName) {
throw new IllegalArgumentException("File name contains null byte.");
}

java.nio.file.Path leafName = java.nio.file.Path.of(fileName.strip()).getFileName();
if (leafName == null) {
String cleanPath = org.springframework.util.StringUtils.cleanPath(fileName.strip());
int lastSlash = cleanPath.lastIndexOf('/');
String normalized = lastSlash != -1 ? cleanPath.substring(lastSlash + 1) : cleanPath;
if (normalized.isEmpty()) {
return "";
}
String normalized = leafName.toString();
int lastDot = normalized.lastIndexOf('.');
if (lastDot <= 0 || lastDot == normalized.length() - 1) {
return "";
Expand Down Expand Up @@ -199,6 +200,9 @@ private byte[] computeSignature(String approverId, String extension, String secr
}

private String tokenFingerprint(String approvalToken) {
if (approvalToken == null) {
return null;
}
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashed = digest.digest(approvalToken.getBytes(StandardCharsets.UTF_8));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ void sanitizeFilenameReturnsNullWhenFilenameIsNull() throws Exception {
DefaultDocumentValidationService validationService = new DefaultDocumentValidationService(conversionProperties);
java.lang.reflect.Method method = DefaultDocumentValidationService.class.getDeclaredMethod("sanitizeFilename", String.class);
method.setAccessible(true);
String sanitized = (String) method.invoke(validationService, new Object[] {null});
String sanitized = (String) method.invoke(validationService, (Object) null);
assertNull(sanitized);
}

Expand All @@ -47,6 +47,46 @@ void sanitizeFilenameReturnsCleanPathWhenNoSlashIsPresent() throws Exception {
assertEquals("simple-file.txt", sanitized);
}

@Test
void sanitizeFilenameReturnsCleanPathWhenMultipleSlashesArePresent() throws Exception {
ConversionProperties conversionProperties = new ConversionProperties();
DefaultDocumentValidationService validationService = new DefaultDocumentValidationService(conversionProperties);
java.lang.reflect.Method method = DefaultDocumentValidationService.class.getDeclaredMethod("sanitizeFilename", String.class);
method.setAccessible(true);
String sanitized = (String) method.invoke(validationService, "some/nested/path/file.txt");
assertEquals("file.txt", sanitized);
}

@Test
void sanitizeFilenameReturnsCleanPathWhenPathTraversalIsAttempted() throws Exception {
ConversionProperties conversionProperties = new ConversionProperties();
DefaultDocumentValidationService validationService = new DefaultDocumentValidationService(conversionProperties);
java.lang.reflect.Method method = DefaultDocumentValidationService.class.getDeclaredMethod("sanitizeFilename", String.class);
method.setAccessible(true);
String sanitized = (String) method.invoke(validationService, "../../../etc/passwd.txt");
assertEquals("passwd.txt", sanitized);
}

@Test
void tokenFingerprintReturnsNullWhenApprovalTokenIsNull() throws Exception {
ConversionProperties conversionProperties = new ConversionProperties();
DefaultDocumentValidationService validationService = new DefaultDocumentValidationService(conversionProperties);
java.lang.reflect.Method method = DefaultDocumentValidationService.class.getDeclaredMethod("tokenFingerprint", String.class);
method.setAccessible(true);
String fingerprint = (String) method.invoke(validationService, (Object) null);
assertNull(fingerprint);
}

@Test
void tokenFingerprintReturnsHashWhenApprovalTokenIsProvided() throws Exception {
ConversionProperties conversionProperties = new ConversionProperties();
DefaultDocumentValidationService validationService = new DefaultDocumentValidationService(conversionProperties);
java.lang.reflect.Method method = DefaultDocumentValidationService.class.getDeclaredMethod("tokenFingerprint", String.class);
method.setAccessible(true);
String fingerprint = (String) method.invoke(validationService, "some-token");
// The exact hash doesn't matter for this test, just that it's not null and of expected length
org.junit.jupiter.api.Assertions.assertNotNull(fingerprint);
}

@Test
void stripsDirectoryTraversalFromFilename() {
Expand Down
Loading