diff --git a/.jules/sentinel.md b/.jules/sentinel.md
index e795cb9dd..3dad7ce72 100644
--- a/.jules/sentinel.md
+++ b/.jules/sentinel.md
@@ -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-20 - Admin API Authorization Check
+**Vulnerability:** The Admin API (`AdminController`) exposed internal admin endpoints (`/api/v1/admin/convert/jobs`, etc.) without any `TenantAccessService` authorization checks.
+**Learning:** Admin endpoints must enforce strict authorization checks, such as `admin:operate`, to prevent unauthorized internal or external users from listing all tenant jobs, deleting them, or retrying dead lettered jobs without authorization.
+**Prevention:** Always inject `TenantAccessService` into admin endpoints and require appropriate cross-tenant admin permissions via `tenantAccessService.require(headers, "admin:operate")`.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1187deb2a..b609a7a34 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,7 @@
### Security
+- Jackson BOM을 `2.22.3`으로 올려 `jackson-databind`의 무제한 type-id 캐시 성장(CVE-2026-91776)과 이차 시간 forward-reference 해석(CVE-2026-91777)을 수정했습니다. 만료된 GHSA-5jmj-h7xm-6q6v OSV 예외를 제거하고 SBOM·제3자 고지문을 새 의존성 그래프와 함께 갱신했습니다.
- `GET /api/v1/convert/jobs/{jobId}/download`가 리소스 조회 전에 전용 `artifact:read` 권한을 검증하고, PDF 저장소 접근 전에 작업의 tenant 소유권을 확인하도록 강화했습니다. `job:read`만으로는 문서 바이트를 읽을 수 없으며, 인증 누락·권한 누락·교차 tenant UUID 접근은 각각 fail closed 처리되고 교차 tenant 요청은 리소스 존재를 숨기는 `404`를 반환합니다.
- Maven XML 테스트 보고서 검증기는 각 `testsuite`의 `tests`, `skipped`, `failures`, `errors` 속성을 모두 필수 증거로 요구합니다. 누락된 결과 수를 암묵적으로 0으로 간주하지 않고 fail closed 처리하며, 각 속성 누락 회귀 테스트를 추가했습니다.
- Maven XML 테스트 보고서 검증기는 UTF-8만 허용하고 UTF-8 BOM은 수용하며, NUL 바이트·DTD·엔터티 선언을 파싱 전에 거부합니다. UTF-16 같은 대체 인코딩으로 위험 선언을 바이트 검사에서 숨기는 우회와 외부 엔터티 읽기·엔터티 확장형 서비스 거부를 회귀 테스트로 차단했습니다.
@@ -36,6 +37,7 @@
### Fixed
+- Enforced `admin:operate` on every Admin API endpoint and repaired the controller test fixture to construct the released `TenantContext` contract directly.
- 뷰어 UI의 재시도 버튼 로딩 상태가 내부 DOM을 손상시키지 않고 안전하게 복원되도록 수정했습니다.
## [0.1.0] - 2026-06-25
diff --git a/docs/legal/2026-07-03-third-party-attribution.md b/docs/legal/2026-07-03-third-party-attribution.md
index 7d691a02f..ad8e3ec6c 100644
--- a/docs/legal/2026-07-03-third-party-attribution.md
+++ b/docs/legal/2026-07-03-third-party-attribution.md
@@ -14,11 +14,11 @@ CycloneDX SBOM. It is engineering evidence, not legal advice.
| Component | Version | License metadata | Package URL |
| --- | --- | --- | --- |
| com.fasterxml.jackson.core:jackson-annotations | 2.22 | Apache-2.0 | `pkg:maven/com.fasterxml.jackson.core/jackson-annotations@2.22?type=jar` |
-| com.fasterxml.jackson.core:jackson-core | 2.22.1 | Apache-2.0 | `pkg:maven/com.fasterxml.jackson.core/jackson-core@2.22.1?type=jar` |
-| com.fasterxml.jackson.core:jackson-databind | 2.22.1 | Apache-2.0 | `pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.22.1?type=jar` |
-| com.fasterxml.jackson.datatype:jackson-datatype-jdk8 | 2.22.1 | Apache-2.0 | `pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jdk8@2.22.1?type=jar` |
-| com.fasterxml.jackson.datatype:jackson-datatype-jsr310 | 2.22.1 | Apache-2.0 | `pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jsr310@2.22.1?type=jar` |
-| com.fasterxml.jackson.module:jackson-module-parameter-names | 2.22.1 | Apache-2.0 | `pkg:maven/com.fasterxml.jackson.module/jackson-module-parameter-names@2.22.1?type=jar` |
+| com.fasterxml.jackson.core:jackson-core | 2.22.3 | Apache-2.0 | `pkg:maven/com.fasterxml.jackson.core/jackson-core@2.22.3?type=jar` |
+| com.fasterxml.jackson.core:jackson-databind | 2.22.3 | Apache-2.0 | `pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.22.3?type=jar` |
+| com.fasterxml.jackson.datatype:jackson-datatype-jdk8 | 2.22.3 | Apache-2.0 | `pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jdk8@2.22.3?type=jar` |
+| com.fasterxml.jackson.datatype:jackson-datatype-jsr310 | 2.22.3 | Apache-2.0 | `pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jsr310@2.22.3?type=jar` |
+| com.fasterxml.jackson.module:jackson-module-parameter-names | 2.22.3 | Apache-2.0 | `pkg:maven/com.fasterxml.jackson.module/jackson-module-parameter-names@2.22.3?type=jar` |
| com.fasterxml:classmate | 1.7.3 | Apache-2.0 | `pkg:maven/com.fasterxml/classmate@1.7.3?type=jar` |
| commons-logging:commons-logging | 1.4.0 | Apache-2.0 | `pkg:maven/commons-logging/commons-logging@1.4.0?type=jar` |
| io.micrometer:micrometer-commons | 1.15.12 | Apache-2.0 | `pkg:maven/io.micrometer/micrometer-commons@1.15.12?type=jar` |
diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md
new file mode 100644
index 000000000..ae321364c
--- /dev/null
+++ b/docs/product-technical-gap-baseline.md
@@ -0,0 +1,59 @@
+# Clearfolio Product and Technical Gap Baseline
+
+## Goal and loop
+
+Clearfolio is the ContextualWisdomLab document-viewer bounded context. Its buyer
+quality loop is review → root-cause repair → exact-head validation → ordinary
+merge. A queued, skipped, stale-head, or fail-closed Check is not passing.
+
+## Current product and technical boundary
+
+- **Product responsibility:** submit documents asynchronously, expose conversion
+ status, render same-origin PDF artifacts, and enforce tenant-scoped access.
+- **Runtime boundary:** Spring WebFlux controllers delegate validation,
+ conversion, state, artifact, authorization, and audit work to their owning
+ services and repositories; no cross-service database access is authorized.
+- **Supply-chain boundary:** Maven dependency policy, CycloneDX SBOM, license
+ policy, third-party attribution, Trivy, OSV, Semgrep, and CodeQL provide
+ fail-closed release evidence.
+
+## PRD and TRD acceptance baseline
+
+| Evidence | Acceptance rule | Status |
+| --- | --- | --- |
+| Product behavior | `mvn -B --no-transfer-progress verify`; zero test failures, errors, or skips; production line and branch coverage 100% | Required on exact repair head |
+| Public contract | Java 21 compilation and public Javadocs complete with warning and deprecation budget 0 | Required on exact repair head |
+| Supply chain | Trivy/OSV/SAST report no actionable finding; committed SBOM and attribution match the resolved dependency graph | #503 Security `36824043955` and SAST `36824043968` GREEN |
+| Review admission | Independent approval and all required terminal Checks bind to the current head | Not yet satisfied |
+
+## Gap and action ledger
+
+| Gap | Exact evidence and RCA | Action | Status |
+| --- | --- | --- | --- |
+| Jackson denial-of-service exposure | clearfolio#660 head `2a6b0b621e075da224210c0ca78db344af5dea52`, Security run `36778995059`, Trivy job `110104143035`: CVE-2026-91776 and CVE-2026-91777 in `jackson-databind` 2.22.2 | Repair canonical dependency owner #503 with Jackson 2.22.3, a downgrade regression test, refreshed SBOM/attribution, and removal of the expired OSV exception | Resolved at source commit `3e9ba61e1aa2a903e920cf832f9daa5467f2e166`; CI `36824043863`, Security `36824043955`, SAST `36824043968`, and fuzz `36824043977` GREEN |
+| Historical evidence drift | #503 review found its original one-file 2.22.2 bump left SBOM, attribution, POM rationale, and expired ignore stale | Keep dependency version and buyer evidence in one PR; run attribution/license drift checks | Resolved; both review threads closed and local evidence contracts GREEN |
+| Merge admission | #503 is Ready after product/security/fuzz validation; its Draft CodeQL run `36824043888` was skipped. #660 is Draft on the vulnerable predecessor head. | Obtain a non-skipped CodeQL verdict and independent approval on the current #503 head, then ordinarily integrate it into consumers and revalidate their new heads | Hold |
+
+## Decision traceability
+
+The selected repair is the upstream Jackson 2.22.3 patch line, released
+2026-09-21 with fixes for both CVEs. Ignoring the findings, weakening Trivy, or
+retaining the expired OSV exception was rejected because each would preserve a
+buyer-visible denial-of-service risk or conceal stale evidence. A new duplicate
+dependency PR was rejected because #503 already owns the Jackson version delta.
+
+## References
+
+FasterXML. (2026, September 21). *Jackson release 2.22.3*.
+https://github.com/FasterXML/jackson/wiki/Jackson-Release-2.22.3
+
+
+## 2026-10-01 — Admin API authorization successor evidence
+
+- PR: [#650](https://github.com/ContextualWisdomLab/clearfolio/pull/650)
+- RED exact head: `c6a2a0aa5227c85e52f9eb2c48c7a1839587b802`
+- RED CI: [run 36624093074, Maven test job 109596509190](https://github.com/ContextualWisdomLab/clearfolio/actions/runs/36624093074/job/109596509190)
+- RED Security: [run 36624093209, Trivy job 109733907158](https://github.com/ContextualWisdomLab/clearfolio/actions/runs/36624093209/job/109733907158)
+- Root causes: the new test called nonexistent `TenantContext.fromClaims`, and the consumer head predated canonical Jackson owner #503.
+- Repair: construct the released `TenantContext` record directly, preserve the complete AdminController authorization delta, and integrate owner #503 as an ordinary second parent with its Jackson 2.22.3 dependency/SBOM/attribution/contracts.
+- Status: **Proposed / Draft / merge HOLD** until the new immutable head has terminal CI, Security, SAST, all fuzz shards, CodeQL, and qualifying independent approval. Predecessor, queued, skipped, cancelled, or pending evidence is not GREEN.
diff --git a/docs/qa/evidence/2026-07-02-krw2b-sale-readiness/sbom-cyclonedx.json b/docs/qa/evidence/2026-07-02-krw2b-sale-readiness/sbom-cyclonedx.json
index 2f9e4c212..481a85169 100644
--- a/docs/qa/evidence/2026-07-02-krw2b-sale-readiness/sbom-cyclonedx.json
+++ b/docs/qa/evidence/2026-07-02-krw2b-sale-readiness/sbom-cyclonedx.json
@@ -1,10 +1,10 @@
{
"bomFormat" : "CycloneDX",
"specVersion" : "1.6",
- "serialNumber" : "urn:uuid:b6017fa5-aa1e-3a06-ab1c-8fd43d993316",
+ "serialNumber" : "urn:uuid:e0113328-1366-42aa-b76f-fd4423d3bd59",
"version" : 1,
"metadata" : {
- "timestamp" : "2026-08-05T12:07:15Z",
+ "timestamp" : "2026-10-01T06:14:49Z",
"lifecycles" : [
{
"phase" : "build"
@@ -693,45 +693,45 @@
},
{
"type" : "library",
- "bom-ref" : "pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jdk8@2.22.1?type=jar",
+ "bom-ref" : "pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jdk8@2.22.3?type=jar",
"publisher" : "FasterXML",
"group" : "com.fasterxml.jackson.datatype",
"name" : "jackson-datatype-jdk8",
- "version" : "2.22.1",
+ "version" : "2.22.3",
"description" : "Add-on module for Jackson (https://github.com/FasterXML/jackson) to support JDK 8 data types.",
"scope" : "required",
"hashes" : [
{
"alg" : "MD5",
- "content" : "58b2d6d030c310d11902a11a47159f7f"
+ "content" : "283c51f09192d5354168e8e1db5bad01"
},
{
"alg" : "SHA-1",
- "content" : "ef21a94e7b5603f57ca1efe9215513d210d96320"
+ "content" : "1c8e07b4eefb9ccb6a2247c54f42f293ff0dc8aa"
},
{
"alg" : "SHA-256",
- "content" : "2d5ee03fcaa017db0c947a5921ede8b39481d1d637af39079840a87d55e907eb"
+ "content" : "f223061a5aaecdc9506458143807c73bcd1a744d6b3bbb8da4703a0abc408f7d"
},
{
"alg" : "SHA-512",
- "content" : "62a533985dee473d3ef39400afb7bb833ce83611d0b2414f3a89bd53bcd9e21f14e63ad94cb2cfb7ae96fa961903fda78e0176d46bbe19473d19c627fdfa648c"
+ "content" : "0ec3c7bae5df6b3140a18a41a784767748870e7e2067592985c68b144b1cd775601902d9d7af7c14d5039e25025cd0f9b95cd6a172f809fd8bb4b5880932067b"
},
{
"alg" : "SHA-384",
- "content" : "423db1c93aa364cd7677eb2971e8354d30be788e8dc6c63cedbe8c784b6439170c3c208c303b96c08ab120ac802c22d1"
+ "content" : "8ef3987a9223e834e529c184cbb00633e3ad7e943ba2b8df2949c8c1d5ae7cb45c490c301a851bf15e8e48dceef041da"
},
{
"alg" : "SHA3-384",
- "content" : "a02e2eaf81ca6869c821015c0d1dec580ed2bd539821a795957b0239834e813db0808144e4ecfbcece992a212117f252"
+ "content" : "2ce308f3b9252489b0453f1e9eee1a8bcfcf255913910e0ef6c7f87d55bcb48d9a1364989bc163ed8aae166acd8b14b5"
},
{
"alg" : "SHA3-256",
- "content" : "a246cfeed6f3f59c48cf6990d6d7adc77637c318401956a8e0ccaa0ed51e9814"
+ "content" : "b6c27271cdf47235c59997fb390d8569b19dc0c82b848daf0d750b67a6b21074"
},
{
"alg" : "SHA3-512",
- "content" : "9808c24c3d1c241ff9c89e621338fcbb483fc1eb6560465562546713506639c5e633df41b844c8bd0f76497027404dd73fe3f720cddd65ec15841f22ec8fc13f"
+ "content" : "c9acd1a2ed3ec82b8411c3fa7c7132784029aaf5226edf3b622ac57862a9d3bebc19fba5be10048853d5b6456c5ce3fcdf9b4962f538480cea42d5698bb2824b"
}
],
"licenses" : [
@@ -741,7 +741,7 @@
}
}
],
- "purl" : "pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jdk8@2.22.1?type=jar",
+ "purl" : "pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jdk8@2.22.3?type=jar",
"externalReferences" : [
{
"type" : "website",
@@ -763,45 +763,45 @@
},
{
"type" : "library",
- "bom-ref" : "pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jsr310@2.22.1?type=jar",
+ "bom-ref" : "pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jsr310@2.22.3?type=jar",
"publisher" : "FasterXML",
"group" : "com.fasterxml.jackson.datatype",
"name" : "jackson-datatype-jsr310",
- "version" : "2.22.1",
+ "version" : "2.22.3",
"description" : "Add-on module to support JSR-310 (Java 8 Date & Time API) data types.",
"scope" : "required",
"hashes" : [
{
"alg" : "MD5",
- "content" : "4dd3e0a2be3f88d720f9214954ed59b6"
+ "content" : "1f14fa9b9779d0545287bba2bfb927df"
},
{
"alg" : "SHA-1",
- "content" : "3377a0589c1b0e62bbe245ac8e9f56fa7d7b8bb9"
+ "content" : "8d54f48cfe4865bdfa245b8a90fb15b56c8a4a3c"
},
{
"alg" : "SHA-256",
- "content" : "85da45b7e5e565418963ff8f0dcb184365b0557c15468933f4318dde5c3ce845"
+ "content" : "ec7052ee48c9d873bb8148338a643f02cf89c01d035dd219411473030d6d2b22"
},
{
"alg" : "SHA-512",
- "content" : "362067f556de43c49829fd5d69681bba3a43bc5a7211811061d62838f047cea368174d865944aa2434f5d7592a2cdbd7a75b534493b0c14e6340f1536718ce41"
+ "content" : "3a711d091436497aa84ea83316f948bbc73644c9a0a8ed478cf76d34b28340d3e596025bf5c8a61ca8ab1806c35f3db5e45d499340d091e224b75270a730b2d2"
},
{
"alg" : "SHA-384",
- "content" : "aec680e1f9f4b48c7cb1b0e6ba1631ee6c69b0d2983da2f0ce26b4e525054fffa0a8d073c1ac37f36b6f004d413abc38"
+ "content" : "38782f7a5b6f857e98362212cb953bfe4fb9f11fed935a6621145fe35c4a2f5afc034c048849d96f490b86bf9c1b69ee"
},
{
"alg" : "SHA3-384",
- "content" : "afc1e60eb4215320d1ae959f2095215f9fa38db4cfe04b2d9ea626938c7469bd9872d3ad83e401ce5e6c0f11f338290e"
+ "content" : "bf40dfc2bccb29fc9eca32455203e7d9d26b18c54ba9fa1f47ce60b45036479d07fcfda880fadc61270d52f9a7d2150d"
},
{
"alg" : "SHA3-256",
- "content" : "a7e4a6a3455c2dfda0a691d413a5afeef1d38587b1ead2318b80f8966d1c783f"
+ "content" : "0d17f4e5f1b237905a3b1d63992a01908250e7a98cd6dff099d59c1e8e202dc0"
},
{
"alg" : "SHA3-512",
- "content" : "58a71d8bad72fada694e5484d99a4625d672d994ffc3b43196642ae48a08c4430fc21e20937f3fa01d0c182136eccfbc09926d11bad267455180fbd9774427c0"
+ "content" : "2153206c85167b6737e4f2cd3dd1e93ed213ebc049bdc8d099b61934301c4c9bc738c5fc19b7f3a778a918d92fbdbfe19df8ccfdc088ed8ee22648417dc99ffe"
}
],
"licenses" : [
@@ -811,7 +811,7 @@
}
}
],
- "purl" : "pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jsr310@2.22.1?type=jar",
+ "purl" : "pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jsr310@2.22.3?type=jar",
"externalReferences" : [
{
"type" : "website",
@@ -833,45 +833,45 @@
},
{
"type" : "library",
- "bom-ref" : "pkg:maven/com.fasterxml.jackson.module/jackson-module-parameter-names@2.22.1?type=jar",
+ "bom-ref" : "pkg:maven/com.fasterxml.jackson.module/jackson-module-parameter-names@2.22.3?type=jar",
"publisher" : "FasterXML",
"group" : "com.fasterxml.jackson.module",
"name" : "jackson-module-parameter-names",
- "version" : "2.22.1",
+ "version" : "2.22.3",
"description" : "Add-on module for Jackson (https://github.com/FasterXML/jackson) to support introspection of method/constructor parameter names, without having to add explicit property name annotation.",
"scope" : "required",
"hashes" : [
{
"alg" : "MD5",
- "content" : "f6bde154ca5466d618a6040e819e35d5"
+ "content" : "f24f70cfa9354d9870e1b2e73076b65d"
},
{
"alg" : "SHA-1",
- "content" : "6776897d180781c6190f0e6832c8e2cdaefe78b9"
+ "content" : "533b13ecee272025f947f9c4c40f178cf72afebe"
},
{
"alg" : "SHA-256",
- "content" : "dc344110fed9779bee54291044d1d209bac31ce9844f5b1a217ce46bd74891f9"
+ "content" : "3f085667cd7b2dd9794a837b6e4a5c57779132bfc37b49b5c457870eee9d60ba"
},
{
"alg" : "SHA-512",
- "content" : "0e853ed90621b7d31426db3f95cf6440fc60c31c24a62082f861f2576f2c911cb18c80b2f629f023eab57d574b97673ec53d39986dd27bda642d318ce51214cd"
+ "content" : "8eaf493277d0a6956d51449e21c76a529f4e4a26f4c518b51fb0d47e3dded0c8d10cc53c9014eeda0813057f49cd48dab371d7dcc41e875c162a9110a4d74b7d"
},
{
"alg" : "SHA-384",
- "content" : "a536e410637e32e1309ecabef684ded3a5c200dab9287d50997ffb14a46f42124fb81acc32a8e12254e9334f26c8d783"
+ "content" : "fe60a1409a2bea0f4305355e13b692361289937808f3d6d0e3338c96450b82f3215a8c1add01b1a2ccffae529ad3aad8"
},
{
"alg" : "SHA3-384",
- "content" : "8a55123d3673fab467d8be5dcbe7c592f5ac5084370dde7ca06a0bc56149fe1152ecbae2e0d5fabd29c3efed1aba039d"
+ "content" : "4ad856d4518a00cf45b58be353f9e8a9975d3a32faa332234b005bfaa113c3e86b7994e3aa76c00e81db2202e64586f1"
},
{
"alg" : "SHA3-256",
- "content" : "45e2f9021d6aa08fea40c0f3ba0257ef381f83e28d96f22c38d5a48de086a64e"
+ "content" : "f66e5ff504b4e190c0288f0da4092653d326a87065cc89b0fdf50b2d55450d65"
},
{
"alg" : "SHA3-512",
- "content" : "cd59874c12e14572fc57a45e0ab3c40dea2fbb3b3ca51bb33f14b0df2cc5d30f7dbca220224349a62e49adc6a030d3699b6704fa15851ab1218e39a5b5296161"
+ "content" : "fef5461656ec8d29bbe97a9629866dfb5b149ea947e0381f8f787fd47e86eeead8fa326aafd6d0d1b5a37abe846f74316ed13fae431b9ef0ade1638883f9d6e1"
}
],
"licenses" : [
@@ -881,7 +881,7 @@
}
}
],
- "purl" : "pkg:maven/com.fasterxml.jackson.module/jackson-module-parameter-names@2.22.1?type=jar",
+ "purl" : "pkg:maven/com.fasterxml.jackson.module/jackson-module-parameter-names@2.22.3?type=jar",
"externalReferences" : [
{
"type" : "website",
@@ -3863,45 +3863,45 @@
},
{
"type" : "library",
- "bom-ref" : "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.22.1?type=jar",
+ "bom-ref" : "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.22.3?type=jar",
"publisher" : "FasterXML",
"group" : "com.fasterxml.jackson.core",
"name" : "jackson-databind",
- "version" : "2.22.1",
+ "version" : "2.22.3",
"description" : "General data-binding functionality for Jackson: works on core streaming API",
"scope" : "required",
"hashes" : [
{
"alg" : "MD5",
- "content" : "df2dcf8d581836d48aae7387311cba93"
+ "content" : "e1fbe6bbbb606fad49068fd2670a5208"
},
{
"alg" : "SHA-1",
- "content" : "9e2fb91831cce9cb9262909cd76647508949f232"
+ "content" : "d0d4b2cdb0a1440ac02178fc2ff01dfb05cd06a8"
},
{
"alg" : "SHA-256",
- "content" : "7dcd7e53bec1f56c7ad278bd1ca0840bebcc595d61ce44d6a8439abb75b965b2"
+ "content" : "556db5439e206114346043f68d200497dc96a0bca62a360a81784092ebd0e0a9"
},
{
"alg" : "SHA-512",
- "content" : "d8a905e80e6d7dd2026b16b075dde2d409425b61417120980ea07b261c6bf98d5c065053f1c063b60da30b4324157778ec4eb859945fb8543176e55a2d3b8db8"
+ "content" : "2c94560ef0f01f7dd12ae0810dbcfaf80fddd889bc25f597350fb75a0fcc266fa68f5795a773f772692426ac81a6ee65ed2bde8db8184bb2788323bc95ef7ebe"
},
{
"alg" : "SHA-384",
- "content" : "b45efa1187b756bdfa7e0c6bc2eda85fcf13d879b4bf370f06ef95325c13b629f763a8027dbc34dab7c6a549bb33e945"
+ "content" : "35a064a6a2a93643e10f87b9e0a0527fa3c12a14de7f76bcdc81ce3939150fd683159c08164011532e84504eb473e277"
},
{
"alg" : "SHA3-384",
- "content" : "7eeb7d7ea5434fdede4b0d256955d0ebf93a11ad89efcc2a3551d649bf197f6aa6c3dd60247805a13270fd8ed0a98ae8"
+ "content" : "40f36ac8e077efe3f141b03f1390cdb38e1c829058fb86510abea9778e792725f55f6684cbba4d366b6055ba44b7d80f"
},
{
"alg" : "SHA3-256",
- "content" : "ab1df340c8e99a3ea6f485b9e3bf0e1cb7b57810b09e9161c8eee38eb8db3e47"
+ "content" : "0ea05dee4c161251abf19cff75fd82b4937d28d346c53cb3fd7c1e062a2184da"
},
{
"alg" : "SHA3-512",
- "content" : "dc3f3df098898dc6bf71fcef0ca232a592f59fb465161b95b03a28eebc9539bede1db83bacaf14dc57328feffc7a4637b911a09356fa4330d93f52131ac7254e"
+ "content" : "e6dc5878852afc9101d66a2c16b91a419179bfc871ec66ddd8590bcfde9b3f2889d4fa317ab8fe40aa2ba6bd94a4aa8a1121ea358e10623de54a678a080f6676"
}
],
"licenses" : [
@@ -3911,7 +3911,7 @@
}
}
],
- "purl" : "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.22.1?type=jar",
+ "purl" : "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.22.3?type=jar",
"externalReferences" : [
{
"type" : "website",
@@ -4003,45 +4003,45 @@
},
{
"type" : "library",
- "bom-ref" : "pkg:maven/com.fasterxml.jackson.core/jackson-core@2.22.1?type=jar",
+ "bom-ref" : "pkg:maven/com.fasterxml.jackson.core/jackson-core@2.22.3?type=jar",
"publisher" : "FasterXML",
"group" : "com.fasterxml.jackson.core",
"name" : "jackson-core",
- "version" : "2.22.1",
+ "version" : "2.22.3",
"description" : "Core Jackson processing abstractions (aka Streaming API), implementation for JSON",
"scope" : "required",
"hashes" : [
{
"alg" : "MD5",
- "content" : "09e866bcd73613665ef49f5086704fb1"
+ "content" : "75691eef4483b7eb0e1c43e08dcb6573"
},
{
"alg" : "SHA-1",
- "content" : "da7ffb60088d7e8f37ecdd3b617520971cc7b9bf"
+ "content" : "8695aab38355c65b60e242ec62edf13c52916d88"
},
{
"alg" : "SHA-256",
- "content" : "941ff029bcdb93e83d209ce516c1a7fb8bbac07d0a2fa122f5bf194b2cd7b4f4"
+ "content" : "8a501126a385b25841915d839508f8a66e2a0dbc8a6709d055ef3b3e852b094c"
},
{
"alg" : "SHA-512",
- "content" : "d4485af4465f561034df86686d672835b0272b6200efb0af55ea71e62d3faf09e9f90e42df4da64650795cf00519fc553f892bf6179330a3e74750110a785efb"
+ "content" : "8d5984961b37f9570d0581a74f005442fe37c46c601ee0fe63e3695f967bd7598dd268cfb33509a289ff472385cedd99396660e31d7cb873d1e7ffff676912e8"
},
{
"alg" : "SHA-384",
- "content" : "faaf92de6e93475fdbb7d56221fd35d8435ac4f87dddfd334ed05c4318b8b72220010effd08bf8dd156420771b294eb4"
+ "content" : "788125564042463c3b3f856d803c955282cacbda361d8dc172fdc8288013dd17db525e490bc0bde360c480e431a7a1d1"
},
{
"alg" : "SHA3-384",
- "content" : "5c432f1a2f3369485c4ed7d3c3c4499342c23efd0d7a574b73421345df9762bd27643cc469b01632400691f3c09b5d8b"
+ "content" : "5706651739544e7216903e0e6c5f4edd362b158b787d04afe0d31b65137394605ed569ecb924728d9cd5a4850fa07b3f"
},
{
"alg" : "SHA3-256",
- "content" : "48fc6e17c36e14b8bae387b92248a43da12293b68a61dd2517c708dbb24ed010"
+ "content" : "f41bb3e563a0b67d3ae1d9c29e0ebe8b86aa650da68b66e66d9403bd6e774c37"
},
{
"alg" : "SHA3-512",
- "content" : "f72ef2d2a1012be8b1c972323527a396bbaea0b543bd79fac6d380f8e4855c6d98190b067ccec92ece9ed8e1ece1e6046b1d93f9c85944fcb7133fe0b4dc60e4"
+ "content" : "3d3116030b1efdab1ca91f0ec21a72bf8f2a0397d1eae70dddc1848906781c22aa941b8a24b47c1a33ede3675a47b12510aeea1a042b198936fc5fbcf7b645e0"
}
],
"licenses" : [
@@ -4051,7 +4051,7 @@
}
}
],
- "purl" : "pkg:maven/com.fasterxml.jackson.core/jackson-core@2.22.1?type=jar",
+ "purl" : "pkg:maven/com.fasterxml.jackson.core/jackson-core@2.22.3?type=jar",
"externalReferences" : [
{
"type" : "website",
@@ -4213,7 +4213,7 @@
"pkg:maven/org.springframework.boot/spring-boot-starter-log4j2@3.5.16?type=jar",
"pkg:maven/org.apache.pdfbox/pdfbox@3.0.8?type=jar",
"pkg:maven/org.webjars.npm/pdfjs-dist@6.1.200?type=jar",
- "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.22.1?type=jar"
+ "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.22.3?type=jar"
]
},
{
@@ -4306,10 +4306,10 @@
"dependsOn" : [
"pkg:maven/org.springframework.boot/spring-boot-starter@3.5.16?type=jar",
"pkg:maven/org.springframework/spring-web@6.2.19?type=jar",
- "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.22.1?type=jar",
- "pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jdk8@2.22.1?type=jar",
- "pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jsr310@2.22.1?type=jar",
- "pkg:maven/com.fasterxml.jackson.module/jackson-module-parameter-names@2.22.1?type=jar"
+ "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.22.3?type=jar",
+ "pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jdk8@2.22.3?type=jar",
+ "pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jsr310@2.22.3?type=jar",
+ "pkg:maven/com.fasterxml.jackson.module/jackson-module-parameter-names@2.22.3?type=jar"
]
},
{
@@ -4321,10 +4321,10 @@
]
},
{
- "ref" : "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.22.1?type=jar",
+ "ref" : "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.22.3?type=jar",
"dependsOn" : [
"pkg:maven/com.fasterxml.jackson.core/jackson-annotations@2.22?type=jar",
- "pkg:maven/com.fasterxml.jackson.core/jackson-core@2.22.1?type=jar"
+ "pkg:maven/com.fasterxml.jackson.core/jackson-core@2.22.3?type=jar"
]
},
{
@@ -4332,29 +4332,29 @@
"dependsOn" : [ ]
},
{
- "ref" : "pkg:maven/com.fasterxml.jackson.core/jackson-core@2.22.1?type=jar",
+ "ref" : "pkg:maven/com.fasterxml.jackson.core/jackson-core@2.22.3?type=jar",
"dependsOn" : [ ]
},
{
- "ref" : "pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jdk8@2.22.1?type=jar",
+ "ref" : "pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jdk8@2.22.3?type=jar",
"dependsOn" : [
- "pkg:maven/com.fasterxml.jackson.core/jackson-core@2.22.1?type=jar",
- "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.22.1?type=jar"
+ "pkg:maven/com.fasterxml.jackson.core/jackson-core@2.22.3?type=jar",
+ "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.22.3?type=jar"
]
},
{
- "ref" : "pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jsr310@2.22.1?type=jar",
+ "ref" : "pkg:maven/com.fasterxml.jackson.datatype/jackson-datatype-jsr310@2.22.3?type=jar",
"dependsOn" : [
"pkg:maven/com.fasterxml.jackson.core/jackson-annotations@2.22?type=jar",
- "pkg:maven/com.fasterxml.jackson.core/jackson-core@2.22.1?type=jar",
- "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.22.1?type=jar"
+ "pkg:maven/com.fasterxml.jackson.core/jackson-core@2.22.3?type=jar",
+ "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.22.3?type=jar"
]
},
{
- "ref" : "pkg:maven/com.fasterxml.jackson.module/jackson-module-parameter-names@2.22.1?type=jar",
+ "ref" : "pkg:maven/com.fasterxml.jackson.module/jackson-module-parameter-names@2.22.3?type=jar",
"dependsOn" : [
- "pkg:maven/com.fasterxml.jackson.core/jackson-core@2.22.1?type=jar",
- "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.22.1?type=jar"
+ "pkg:maven/com.fasterxml.jackson.core/jackson-core@2.22.3?type=jar",
+ "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.22.3?type=jar"
]
},
{
diff --git a/osv-scanner.toml b/osv-scanner.toml
deleted file mode 100644
index a6aed8eb0..000000000
--- a/osv-scanner.toml
+++ /dev/null
@@ -1,4 +0,0 @@
-[[IgnoredVulns]]
-id = "GHSA-5jmj-h7xm-6q6v"
-ignoreUntil = 2026-08-15
-reason = "jackson-databind is pinned to 2.22.1, which this advisory's own patched-version text identifies as fixed for CVE-2026-54515; OSV Scanner v2.3.8 still matches 2.22.1 via a broad Maven range."
diff --git a/pom.xml b/pom.xml
index af3ec7ad2..824c6b3cf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -38,12 +38,10 @@
which contains the July 2026 HTTP, HTTP/2, MQTT, compression, and
parser-boundary hardening release. -->
4.1.136.Final
-
- 2.22.1
+
+ 2.22.3
1.5.35
@@ -59,7 +57,7 @@
-->
-
+
com.fasterxml.jackson
jackson-bom
@@ -265,4 +263,4 @@
-
\ No newline at end of file
+
diff --git a/scripts/test_render_third_party_attribution.py b/scripts/test_render_third_party_attribution.py
old mode 100755
new mode 100644
index 767c5202e..379014386
--- a/scripts/test_render_third_party_attribution.py
+++ b/scripts/test_render_third_party_attribution.py
@@ -29,6 +29,9 @@
NETTY_VERSION_PATTERN = re.compile(
r"\s*([^<\s]+)\s*"
)
+JACKSON_VERSION_PATTERN = re.compile(
+ r"\s*([^<\s]+)\s*"
+)
def component(group: str, name: str, version: str, license_id: str, purl: str) -> dict:
@@ -50,6 +53,16 @@ def managed_netty_version() -> str:
return matches[0]
+def managed_jackson_version() -> str:
+ """Read the reviewed Jackson family version from the trusted project POM."""
+ matches = JACKSON_VERSION_PATTERN.findall(POM_PATH.read_text(encoding="utf-8"))
+ if len(matches) != 1:
+ raise AssertionError(
+ "pom.xml must declare exactly one non-blank jackson-bom.version property"
+ )
+ return matches[0]
+
+
class ThirdPartyAttributionTest(unittest.TestCase):
"""Protect rendering behavior and buyer-evidence dependency consistency."""
@@ -193,6 +206,51 @@ def test_buyer_evidence_tracks_reviewed_netty_security_line(self) -> None:
"the historical Netty line must be absent from buyer attribution",
)
+ def test_buyer_evidence_tracks_reviewed_jackson_security_line(self) -> None:
+ """Require Jackson SBOM references and attribution to match Maven."""
+ expected_version = managed_jackson_version()
+ sbom_text = SBOM_PATH.read_text(encoding="utf-8")
+ sbom = json.loads(sbom_text)
+ jackson_components = [
+ item
+ for item in sbom.get("components", [])
+ if str(item.get("group", "")).startswith("com.fasterxml.jackson")
+ and item.get("name") != "jackson-annotations"
+ ]
+
+ self.assertGreater(len(jackson_components), 0)
+ self.assertEqual(
+ {expected_version},
+ {str(item.get("version", "")) for item in jackson_components},
+ "every patch-versioned Jackson module must match jackson-bom.version",
+ )
+ for item in jackson_components:
+ coordinate = f"{item.get('group')}:{item.get('name')}"
+ self.assertIn(
+ f"@{expected_version}",
+ str(item.get("purl", "")),
+ f"{coordinate} purl must identify the reviewed Jackson version",
+ )
+ self.assertIn(
+ f"@{expected_version}",
+ str(item.get("bom-ref", "")),
+ f"{coordinate} bom-ref must identify the reviewed Jackson version",
+ )
+
+ self.assertNotIn("2.22.1", sbom_text)
+ actual_attribution = ATTRIBUTION_PATH.read_text(encoding="utf-8")
+ jackson_rows = [
+ line
+ for line in actual_attribution.splitlines()
+ if line.startswith("| com.fasterxml.jackson.")
+ and ":jackson-annotations |" not in line
+ ]
+ self.assertEqual(len(jackson_components), len(jackson_rows))
+ self.assertTrue(
+ all(f"| {expected_version} |" in line for line in jackson_rows),
+ "every patch-versioned Jackson attribution row must use the reviewed version",
+ )
+
if __name__ == "__main__":
unittest.main()
diff --git a/src/main/java/com/clearfolio/viewer/controller/AdminController.java b/src/main/java/com/clearfolio/viewer/controller/AdminController.java
index 412d4eb86..cfb296e1e 100644
--- a/src/main/java/com/clearfolio/viewer/controller/AdminController.java
+++ b/src/main/java/com/clearfolio/viewer/controller/AdminController.java
@@ -11,10 +11,13 @@
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.http.HttpHeaders;
+import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import com.clearfolio.viewer.api.AdminJobListResponse;
+import com.clearfolio.viewer.auth.TenantAccessService;
import com.clearfolio.viewer.model.ConversionJob;
import com.clearfolio.viewer.service.DocumentConversionService;
import com.clearfolio.viewer.service.RetryDeadLetterResult;
@@ -25,25 +28,41 @@
@RestController
public class AdminController {
+ /**
+ * Conversion service for managing jobs.
+ */
private final DocumentConversionService conversionService;
+ /**
+ * Service to enforce access controls.
+ */
+ private final TenantAccessService tenantAccessService;
+
/**
* Creates a controller for admin operations.
*
- * @param conversionService conversion service
+ * @param conversionServiceInjected conversion service
+ * @param tenantAccessServiceInjected access control service
*/
- public AdminController(DocumentConversionService conversionService) {
- this.conversionService = conversionService;
+ public AdminController(
+ final DocumentConversionService conversionServiceInjected,
+ final TenantAccessService tenantAccessServiceInjected) {
+ this.conversionService = conversionServiceInjected;
+ this.tenantAccessService = tenantAccessServiceInjected;
}
/**
* Retrieves all conversion jobs, optionally filtered by dead-letter status.
*
* @param deadLettered optional filter for dead-lettered jobs
+ * @param headers request headers carrying tenant claims
* @return list of conversion jobs
*/
@GetMapping("/api/v1/admin/convert/jobs")
- public AdminJobListResponse getAllJobs(@RequestParam(required = false) Boolean deadLettered) {
+ public AdminJobListResponse getAllJobs(
+ @RequestParam(required = false) final Boolean deadLettered,
+ @RequestHeader final HttpHeaders headers) {
+ tenantAccessService.require(headers, "admin:operate");
Iterable allJobs = conversionService.getAllJobs();
if (deadLettered == null) {
@@ -63,10 +82,14 @@ public AdminJobListResponse getAllJobs(@RequestParam(required = false) Boolean d
* Deletes a conversion job.
*
* @param jobId conversion job identifier
+ * @param headers request headers carrying tenant claims
* @return no content on success
*/
@DeleteMapping("/api/v1/admin/convert/jobs/{jobId}")
- public ResponseEntity deleteJob(@PathVariable UUID jobId) {
+ public ResponseEntity deleteJob(
+ @PathVariable final UUID jobId,
+ @RequestHeader final HttpHeaders headers) {
+ tenantAccessService.require(headers, "admin:operate");
conversionService.deleteJob(jobId);
return ResponseEntity.noContent().build();
}
@@ -75,16 +98,23 @@ public ResponseEntity deleteJob(@PathVariable UUID jobId) {
* Retries a dead-lettered conversion job.
*
* @param jobId conversion job identifier
+ * @param headers request headers carrying tenant claims
* @return accepted response on success
*/
@PostMapping("/api/v1/admin/convert/jobs/{jobId}/retry")
- public ResponseEntity retryDeadLettered(@PathVariable UUID jobId) {
- RetryDeadLetterResult result = conversionService.retryDeadLettered(jobId, "admin");
+ public ResponseEntity retryDeadLettered(
+ @PathVariable final UUID jobId,
+ @RequestHeader final HttpHeaders headers) {
+ tenantAccessService.require(headers, "admin:operate");
+ RetryDeadLetterResult result = conversionService.retryDeadLettered(
+ jobId, "admin");
if (result == RetryDeadLetterResult.NOT_FOUND) {
- throw new ResponseStatusException(HttpStatus.NOT_FOUND, "job not found");
+ throw new ResponseStatusException(
+ HttpStatus.NOT_FOUND, "job not found");
}
if (result == RetryDeadLetterResult.NOT_ELIGIBLE) {
- throw new ResponseStatusException(HttpStatus.CONFLICT, "job is not eligible for retry");
+ throw new ResponseStatusException(
+ HttpStatus.CONFLICT, "job is not eligible for retry");
}
return ResponseEntity.accepted().build();
}
diff --git a/src/test/java/com/clearfolio/viewer/config/DependencyPolicyTest.java b/src/test/java/com/clearfolio/viewer/config/DependencyPolicyTest.java
index 2c3f0f2b5..ce00abbe0 100644
--- a/src/test/java/com/clearfolio/viewer/config/DependencyPolicyTest.java
+++ b/src/test/java/com/clearfolio/viewer/config/DependencyPolicyTest.java
@@ -53,6 +53,18 @@ void pomPinsPatchedNettyLineForReactiveHttpServing() throws Exception {
);
}
+ @Test
+ void pomPinsJacksonLineWithBoundedDeserializationWork() throws Exception {
+ Document document = parsedPom();
+ Element properties = (Element) document.getElementsByTagName("properties").item(0);
+
+ assertEquals(
+ "2.22.3",
+ directChildTextOf(properties, "jackson-bom.version"),
+ "Jackson 2.22.3 fixes CVE-2026-91776 and CVE-2026-91777 denial-of-service paths"
+ );
+ }
+
@Test
void mavenVerifyGeneratesWarningFreePublicApiJavadocs() throws Exception {
Document document = parsedPom();
diff --git a/src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java b/src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java
index ad63a8015..5a537b6b9 100644
--- a/src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java
+++ b/src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java
@@ -3,13 +3,20 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+
import java.util.Arrays;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.springframework.http.HttpHeaders;
import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.web.server.ResponseStatusException;
+import com.clearfolio.viewer.auth.TenantAccessService;
+import com.clearfolio.viewer.auth.TenantContext;
import com.clearfolio.viewer.model.ConversionJob;
import com.clearfolio.viewer.service.DocumentConversionService;
import com.clearfolio.viewer.service.RetryDeadLetterResult;
@@ -17,13 +24,18 @@
class AdminControllerTest {
private DocumentConversionService conversionService;
+ private TenantAccessService tenantAccessService;
private WebTestClient webTestClient;
private AdminController controller;
@BeforeEach
void setUp() {
conversionService = mock(DocumentConversionService.class);
- controller = new AdminController(conversionService);
+ tenantAccessService = mock(TenantAccessService.class);
+ TenantContext tenantContext = new TenantContext("tenant-1", "user-1", java.util.Set.of("admin:operate"));
+ when(tenantAccessService.require(any(HttpHeaders.class), eq("admin:operate"))).thenReturn(tenantContext);
+
+ controller = new AdminController(conversionService, tenantAccessService);
webTestClient = WebTestClient.bindToController(controller)
.controllerAdvice(new ApiExceptionHandler())
.build();
@@ -121,4 +133,15 @@ void retryDeadLetteredReturnsConflictWhenNotEligible() {
.exchange()
.expectStatus().isEqualTo(409); // isConflict() isn't always available depending on spring-test version, so using isEqualTo(409) is safer
}
+
+ @Test
+ void rejectsUnauthorizedAccess() {
+ when(tenantAccessService.require(any(HttpHeaders.class), eq("admin:operate")))
+ .thenThrow(new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.FORBIDDEN));
+
+ webTestClient.get()
+ .uri("/api/v1/admin/convert/jobs")
+ .exchange()
+ .expectStatus().isForbidden();
+ }
}