diff --git a/.jules/bolt.md b/.jules/bolt.md index 284d180a7..d5089ff73 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -19,3 +19,6 @@ ## 2026-07-13 - 단일 패스 문자열 치환 최적화 (O(N) 단일 스캔 및 지연 할당) **Learning:** `String.replace()`를 여러 번 체이닝하여 호출하면, 문자열 치환이 발생하지 않는 경우에도 내부적으로 불필요한 스캔이 중복 발생하고, 치환 시마다 새로운 문자열 객체와 char 배열이 할당되어 메모리 낭비와 성능 저하(GC 압박)가 발생한다. **Action:** 여러 문자를 한 번에 치환해야 하는 경우, O(N) 단일 스캔을 통해 `charAt()`으로 문자를 확인하고, 치환이 실제로 필요한 경우에만 `StringBuilder`를 지연 할당(Lazy allocation)하여 성능을 최적화하고 불필요한 메모리 할당을 방지한다. +## 2023-11-20 - [String.join Performance Issue] +**Learning:** `String.join` internally creates varargs and Stream processing overhead when used to serialize structured ledger records. +**Action:** Replace `String.join` in `ArtifactLinkLedger` and `KpiSnapshotLedger` with chained `StringBuilder.append()` for significant memory and processing optimization. diff --git a/.jules/sentinel.md b/.jules/sentinel.md index e795cb9dd..850e578c9 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -32,3 +32,11 @@ **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-09-30 - [jackson-databind vulnerabilities] +**Vulnerability:** Trivy-FS detected multiple High and Medium severity CVEs (CVE-2026-68497, CVE-2026-91776, CVE-2026-91777, etc.) associated with `jackson-databind` inside `pom.xml`. +**Learning:** `jackson-databind` vulnerabilities regularly surface when BOM properties referencing out-of-date versions (like `2.22.1`) are present in Maven `pom.xml`. +**Prevention:** Increment the BOM version parameter (e.g. `jackson-bom.version`) in `pom.xml` instead of defining a direct dependency override, ensuring all Jackson-related packages are upgraded symmetrically to resolve vulnerabilities. +## 2026-09-30 - [jackson-databind vulnerabilities part 2] +**Vulnerability:** Jackson `2.22.2` is affected by CVE-2026-91776 and CVE-2026-91777 and was flagged by an exact-head admission correction rule. +**Learning:** Checking the latest versions or canonical owner repositories for terminal GREEN status is necessary when fixing vulnerabilities. +**Prevention:** Always bump to the safe root-cause repair version (`2.22.3`) as verified by the canonical dependency owner to ensure the PR can be ordinarily integrated. diff --git a/pom.xml b/pom.xml index af3ec7ad2..1e5de5f50 100644 --- a/pom.xml +++ b/pom.xml @@ -43,7 +43,7 @@ OSV config contains a narrow temporary ignore because OSV Scanner v2.3.8 still reports 2.22.1 even though the advisory text names it as patched. --> - 2.22.1 + 2.22.3 1.5.35 diff --git a/src/main/java/com/clearfolio/viewer/analytics/KpiSnapshotLedger.java b/src/main/java/com/clearfolio/viewer/analytics/KpiSnapshotLedger.java index f5cbde5d5..144462ccf 100644 --- a/src/main/java/com/clearfolio/viewer/analytics/KpiSnapshotLedger.java +++ b/src/main/java/com/clearfolio/viewer/analytics/KpiSnapshotLedger.java @@ -148,20 +148,20 @@ private void appendLine(String line) { } private static String serialize(KpiSnapshotRecord record) { - return String.join("\t", - SNAPSHOT, - field(record.tenantId()), - field(record.subjectId()), - field(record.exportedAt()), - String.valueOf(record.totalJobs()), - String.valueOf(record.submittedJobs()), - String.valueOf(record.processingJobs()), - String.valueOf(record.succeededJobs()), - String.valueOf(record.failedJobs()), - String.valueOf(record.deadLetteredJobs()), - String.valueOf(record.conversionSuccessRate()), - field(record.p95TimeToPreviewMs()) - ); + return new StringBuilder(256) + .append(SNAPSHOT).append('\t') + .append(field(record.tenantId())).append('\t') + .append(field(record.subjectId())).append('\t') + .append(field(record.exportedAt())).append('\t') + .append(record.totalJobs()).append('\t') + .append(record.submittedJobs()).append('\t') + .append(record.processingJobs()).append('\t') + .append(record.succeededJobs()).append('\t') + .append(record.failedJobs()).append('\t') + .append(record.deadLetteredJobs()).append('\t') + .append(record.conversionSuccessRate()).append('\t') + .append(field(record.p95TimeToPreviewMs())) + .toString(); } private static String field(String value) { diff --git a/src/main/java/com/clearfolio/viewer/artifact/ArtifactLinkLedger.java b/src/main/java/com/clearfolio/viewer/artifact/ArtifactLinkLedger.java index 897b95b2e..2e7567a3f 100644 --- a/src/main/java/com/clearfolio/viewer/artifact/ArtifactLinkLedger.java +++ b/src/main/java/com/clearfolio/viewer/artifact/ArtifactLinkLedger.java @@ -232,46 +232,46 @@ private void appendLine(String line) { } private static String serializeIssued(ArtifactLinkRecord record) { - return String.join("\t", - ISSUED, - field(record.tokenId()), - field(record.tenantId()), - field(record.subjectId()), - record.docId().toString(), - field(record.scope()), - field(record.purpose()), - field(record.artifactChecksum()), - field(record.viewerSessionId()), - field(record.issuedAt()), - field(record.expiresAt()), - field(record.revokedAt()), - field(record.revokedBy()), - field(record.revokeReason()) - ); + return new StringBuilder(256) + .append(ISSUED).append('\t') + .append(field(record.tokenId())).append('\t') + .append(field(record.tenantId())).append('\t') + .append(field(record.subjectId())).append('\t') + .append(record.docId().toString()).append('\t') + .append(field(record.scope())).append('\t') + .append(field(record.purpose())).append('\t') + .append(field(record.artifactChecksum())).append('\t') + .append(field(record.viewerSessionId())).append('\t') + .append(field(record.issuedAt())).append('\t') + .append(field(record.expiresAt())).append('\t') + .append(field(record.revokedAt())).append('\t') + .append(field(record.revokedBy())).append('\t') + .append(field(record.revokeReason())) + .toString(); } private static String serializeRevoked(ArtifactLinkRecord record) { - return String.join("\t", - REVOKED, - field(record.tokenId()), - field(record.revokedAt()), - field(record.revokedBy()), - field(record.revokeReason()) - ); + return new StringBuilder(128) + .append(REVOKED).append('\t') + .append(field(record.tokenId())).append('\t') + .append(field(record.revokedAt())).append('\t') + .append(field(record.revokedBy())).append('\t') + .append(field(record.revokeReason())) + .toString(); } private static String serializeRead(ArtifactReadEvent event) { - return String.join("\t", - READ, - field(event.tenantId()), - field(event.subjectId()), - event.docId().toString(), - field(event.tokenId()), - field(event.rangeRequested()), - String.valueOf(event.statusCode()), - field(event.traceId()), - field(event.readAt()) - ); + return new StringBuilder(192) + .append(READ).append('\t') + .append(field(event.tenantId())).append('\t') + .append(field(event.subjectId())).append('\t') + .append(event.docId().toString()).append('\t') + .append(field(event.tokenId())).append('\t') + .append(field(event.rangeRequested())).append('\t') + .append(event.statusCode()).append('\t') + .append(field(event.traceId())).append('\t') + .append(field(event.readAt())) + .toString(); } private static String field(String value) {