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
7 changes: 7 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Sentinel Journal

## 2026-06-30 - Prevent DOM-based XSS in Viewer JS
**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.
Expand Down Expand Up @@ -32,3 +34,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-14 - 정책 재정의 로그에 존재하는 PII 취약점 패치
**Vulnerability:** 정책 재정의 시 승인자 식별자(`approverId`)가 `DefaultDocumentValidationService`의 `LOGGER.info` 구문을 통해 평문으로 로그에 기록되어 PII(개인 식별 정보) 로깅 정책을 위반했습니다.
**Learning:** 제어 문자에 대한 무해화(`sanitizeForLog` 등) 작업만 수행하는 것만으로는 충분하지 않으며, 평문 식별자가 감사 로그로 노출될 경우 치명적인 PII 유출 취약점을 만들 수 있습니다.
**Prevention:** 민감한 식별자는 로그에 남기기 전 항상 SHA-256과 같은 암호화 해시 함수로 변환(fingerprint)해야 합니다. 또한 로깅 과정 중 식별자가 `null`인 경우 `NullPointerException`이 발생하지 않도록 안전하게 처리해야 합니다.
9 changes: 3 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
## [Unreleased]
### Added
- **UI UX 개선**: 'Details' 버튼 클릭 시, 작업 상세 정보 로드 중에 사용자가 명시적인 로딩 상태를 확인할 수 있도록 'Loading...' 텍스트와 비활성화 상태를 표시하도록 추가했습니다.

# Changelog

## [Unreleased]

### 추가된 기능 (Added)
- **PII 로깅 취약점 방지**: `DefaultDocumentValidationService`에서 평문으로 노출되던 승인자 식별자(`approverId`)를 로깅 전 해싱하도록 수정했습니다.
- **UI UX 개선**: 'Details' 버튼 클릭 시, 작업 상세 정보 로드 중에 사용자가 명시적인 로딩 상태를 확인할 수 있도록 'Loading...' 텍스트와 비활성화 상태를 표시하도록 추가했습니다.
- **관리자용 단건 작업 삭제 및 재시도 API 추가**
- 특정 변환 작업을 삭제할 수 있는 `DELETE /api/v1/admin/convert/jobs/{jobId}` 엔드포인트를 추가했습니다.
- 실패(dead-lettered) 상태인 작업을 관리자가 재시도 큐에 등록할 수 있는 `POST /api/v1/admin/convert/jobs/{jobId}/retry` 엔드포인트를 추가했습니다.
Expand All @@ -15,10 +13,9 @@
- KPI 스냅샷 증거를 다시 불러오는 `refreshKpiEvidence` 동작 중에 "Refresh evidence" 버튼을 비활성화하고 "Refreshing..." 이라는 피드백을 제공하여 사용자의 중복 클릭을 방지했습니다.
- 버튼 상태 변경 시 내부 DOM 구조를 보존하기 위해 `Array.from(button.childNodes)`로 원래 노드를 저장하고, 성공 및 실패 후 `finally` 블록에서 `replaceChildren(...)`으로 안전하게 복원하도록 구현했습니다.


## [0.1.0] - 2026-06-25

### 추가된 기능 (Added)
### 추가된 기능 (Added, 0.1.0)
- **비동기 버튼 로딩 상태 UX 개선 (Async Button Loading States)**
- 문서 제출(`submitDocument`), 데모 데이터 로드(`loadDemoData`), 실패 작업 재시도(`retryActiveJob`) 등 비동기 요청을 수행하는 버튼들에 대해 처리 중 명시적인 로딩 상태(Loading, Submitting, Retrying 등)를 추가했습니다.
- 사용자의 중복 클릭을 방지하기 위해 작업 중에는 버튼이 비활성화되도록 수정했습니다.
Expand Down
9 changes: 6 additions & 3 deletions commit_message.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
🛡️ Sentinel: [CRITICAL] 파일 업로드 경로 조작(Path Traversal) 취약점 수정
정책 재정의 시 PII 노출 취약점 패치

MultipartFile.getOriginalFilename()을 신뢰하여 발생할 수 있는 경로 조작 취약점을 수정했습니다.
StringUtils.cleanPath()를 사용하여 경로를 정규화하고 순수한 파일명만 추출하여 악의적인 페이로드(예: ../../../etc/passwd.hwp)로부터 시스템을 보호합니다.
- `DefaultDocumentValidationService`에서 평문으로 노출되던 승인자 식별자(`approverId`)를 로깅 전 해싱하도록 수정
- 기존 `tokenFingerprint` 메서드를 `fingerprint`로 일반화하고 널 포인터 예외(NPE)를 방지하기 위해 널 체크 추가
- 변경 사항에 대해 100% 테스트 커버리지를 보장하는 반사(reflection) 기반 유닛 테스트 추가
- `.jules/sentinel.md`에 보안 발견 사항 기록 및 오타 수정
- CHANGELOG 업데이트 (PII 로깅 취약점 패치 내역 추가 및 마크다운 린트 규칙 위반사항 수정)
15 changes: 10 additions & 5 deletions description.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
🚨 Severity: CRITICAL
💡 Vulnerability: 파일 업로드 시 `MultipartFile.getOriginalFilename()` 값을 검증 없이 사용하여 발생할 수 있는 경로 조작(Path Traversal) 취약점 발견.
🎯 Impact: 공격자가 디렉토리 탐색 문자열(`../`)을 포함한 파일명을 전송하여 의도하지 않은 경로에 파일을 저장하거나 시스템 파일(예: `/etc/passwd`)에 접근/조작할 위험이 있음.
🔧 Fix: `DefaultDocumentConversionService` 및 `DefaultDocumentValidationService`에서 파일명을 사용하기 전 `StringUtils.cleanPath()`를 통해 경로를 정규화하고, 마지막 `/` 이후의 순수한 파일명만 추출하도록 `sanitizeFilename` 메소드를 추가하여 안전하게 처리함.
✅ Verification: 단위 테스트(`submitStripsDirectoryTraversalFromOriginalFilename` 및 `stripsDirectoryTraversalFromFilename` 등)를 추가하여 취약점 문자열이 정상적으로 제거되며 100% 테스트 커버리지를 보장함.
🚨 Severity: CRITICAL/HIGH
💡 Vulnerability: 정책 재정의 시 승인자 식별자(`approverId`)가 평문으로 `LOGGER.info`를 통해 기록되어 PII 유출 취약점 발생
🎯 Impact: 민감한 PII(개인 식별 정보)가 감사 로그에 노출될 수 있음
🔧 Fix:
- 기존의 `tokenFingerprint`를 `fingerprint`로 일반화하여 승인자 식별자와 토큰 모두에 SHA-256 해싱 적용
- 로그에 기록하기 전 `approverId`가 널일 때 NPE가 발생하지 않도록 널 체킹 도입
- `DefaultDocumentValidationServiceTest`에 100% 분기 검증을 위한 테스트 추가
- `.jules/sentinel.md` 오타(지만 -> or) 수정 및 마크다운 린트 규칙 적용
- CHANGELOG 업데이트 및 마크다운 린트 규칙 적용
✅ Verification: `mvn clean test`를 통해 전체 테스트가 통과하는지 확인. `jacoco.csv` 결과 100% 커버리지 확인 완료.
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ public void validateOrThrow(MultipartFile file, PolicyOverrideRequest overrideRe
LOGGER.info(
"Blocked-format override accepted extension={} approverId={} tokenFingerprint={}",
sanitizeForLog(extension),
sanitizeForLog(overrideApproverIdForAudit),
tokenFingerprint(overrideTokenForAudit)
fingerprint(overrideApproverIdForAudit),
fingerprint(overrideTokenForAudit)
);
}
}
Expand Down Expand Up @@ -198,10 +198,13 @@ private byte[] computeSignature(String approverId, String extension, String secr
}
}

private String tokenFingerprint(String approvalToken) {
private String fingerprint(String data) {
if (data == null) {
return null;
}
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashed = digest.digest(approvalToken.getBytes(StandardCharsets.UTF_8));
byte[] hashed = digest.digest(data.getBytes(StandardCharsets.UTF_8));
// Reused HexFormat for performance
return HEX_FORMAT.formatHex(hashed, 0, FINGERPRINT_TRUNCATE_BYTES);
} catch (NoSuchAlgorithmException ex) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -558,4 +558,14 @@ void throwsWhenSha256DigestIsUnavailableForOverrideAuditFingerprint() {
}
}
}

@Test
void fingerprintReturnsNullWhenDataIsNull() throws Exception {
ConversionProperties conversionProperties = new ConversionProperties();
DefaultDocumentValidationService validationService = new DefaultDocumentValidationService(conversionProperties);
Method method = DefaultDocumentValidationService.class.getDeclaredMethod("fingerprint", String.class);
method.setAccessible(true);
String fingerprinted = (String) method.invoke(validationService, (String) null);
assertNull(fingerprinted);
}
}
Loading