Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
eb4b7cc
๐Ÿ›ก๏ธ Sentinel: [HIGH] ๊ด€๋ฆฌ์ž ์—”๋“œํฌ์ธํŠธ ๊ถŒํ•œ ๋ถ€์—ฌ ์ทจ์•ฝ์  ์ˆ˜์ •
seonghobae Sep 30, 2026
84de07a
๐Ÿ›ก๏ธ Sentinel: [HIGH] ๊ด€๋ฆฌ์ž ์—”๋“œํฌ์ธํŠธ ๊ถŒํ•œ ๋ถ€์—ฌ ์ทจ์•ฝ์  ์ˆ˜์ •
seonghobae Sep 30, 2026
c91387a
๐Ÿ›ก๏ธ Sentinel: [HIGH] ๊ด€๋ฆฌ์ž ์—”๋“œํฌ์ธํŠธ ๊ถŒํ•œ ๋ถ€์—ฌ ์ทจ์•ฝ์  ์ˆ˜์ •
seonghobae Sep 30, 2026
02999de
๐Ÿ›ก๏ธ Sentinel: [HIGH] ๊ด€๋ฆฌ์ž ์—”๋“œํฌ์ธํŠธ ๊ถŒํ•œ ๋ถ€์—ฌ ์ทจ์•ฝ์  ์ˆ˜์ •
seonghobae Sep 30, 2026
769966e
๐Ÿ›ก๏ธ Sentinel: [HIGH] ๊ด€๋ฆฌ์ž ์—”๋“œํฌ์ธํŠธ ๊ถŒํ•œ ๋ถ€์—ฌ ์ทจ์•ฝ์  ์ˆ˜์ •
seonghobae Sep 30, 2026
5ffd3ef
๐Ÿ›ก๏ธ Sentinel: [HIGH] ๊ด€๋ฆฌ์ž ์—”๋“œํฌ์ธํŠธ ๊ถŒํ•œ ๋ถ€์—ฌ ์ทจ์•ฝ์  ์ˆ˜์ •
seonghobae Sep 30, 2026
74708bc
docs: use top-level sentinel heading
seonghobae Sep 30, 2026
945c8be
test(security): guard patched Jackson line
seonghobae Sep 30, 2026
3ccf50e
fix(security): upgrade Jackson to 2.22.3
seonghobae Oct 1, 2026
7b90d4f
docs(security): record Jackson advisory repair
seonghobae Oct 1, 2026
cd9fe4c
docs(gap): establish product technical baseline
seonghobae Oct 1, 2026
ed155fb
test(auth): require tenant admin operation grant
seonghobae Oct 1, 2026
ad8c7a5
fix(auth): grant tenant admins the enforced operation scope
seonghobae Oct 1, 2026
1e6129e
๐Ÿ›ก๏ธ Sentinel: [CRITICAL] AdminController ๊ถŒํ•œ ๋ถ€์—ฌ ๋ˆ„๋ฝ ๊ต์ •
seonghobae Oct 1, 2026
47070e5
๐Ÿ›ก๏ธ Sentinel: [CRITICAL] AdminController ๊ถŒํ•œ ๋ถ€์—ฌ ๋ˆ„๋ฝ ๊ต์ •
seonghobae Oct 1, 2026
a51e19b
test(deps): restore Jackson security floor contract
seonghobae Oct 1, 2026
b20bd4e
test(auth): restore tenant-admin permission contract
seonghobae Oct 1, 2026
69d6f73
docs(gap): restore current product-technical baseline
seonghobae Oct 1, 2026
63529a0
docs(security): preserve top-level ledger heading
seonghobae Oct 1, 2026
9d97f2a
docs(deps): restore complete Jackson advisory provenance
seonghobae Oct 1, 2026
4ba2c0a
๐Ÿ›ก๏ธ Sentinel: [CRITICAL] AdminController ๊ถŒํ•œ ๋ถ€์—ฌ ๋ˆ„๋ฝ ๊ต์ •
seonghobae Oct 1, 2026
ba520f3
๐Ÿ›ก๏ธ Sentinel: [CRITICAL] AdminController ๊ถŒํ•œ ๋ถ€์—ฌ ๋ˆ„๋ฝ ๊ต์ •
seonghobae Oct 1, 2026
2b09b2c
๐Ÿ›ก๏ธ Sentinel: [CRITICAL] AdminController ๊ถŒํ•œ ๋ถ€์—ฌ ๋ˆ„๋ฝ ๊ต์ •
seonghobae Oct 1, 2026
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: 6 additions & 1 deletion .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## 2026-06-30 - Prevent DOM-based XSS in Viewer JS
# 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.
**Prevention:** Implement an `isSafeUrl` verification function to ensure the protocol is strictly `http:` or `https:` (using `new URL()`) before assigning untrusted inputs to DOM attributes like `href` or `src`.
Expand Down 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-10-01 - Add Authentication to Admin Endpoints
**Vulnerability:** Admin endpoints (`AdminController.java`) are completely open without any authentication or authorization checks.
**Learning:** Controller classes meant for internal administration were not integrated with the `TenantAccessService` used across other protected controllers (like `ConversionController`), leading to insecure direct access to jobs.
**Prevention:** Always verify that every controller endpoint is protected by appropriate authorization checks, like `TenantAccessService.require(headers, permission)`.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Added

- **๊ด€๋ฆฌ์ž ์—”๋“œํฌ์ธํŠธ ๋ณด์•ˆ ๊ฐ•ํ™”**: ๊ด€๋ฆฌ์ž ์ „์šฉ API(`AdminController`)์— `TenantAccessService`๋ฅผ ํ†ตํ•œ `ADMIN_OPERATE` ๊ถŒํ•œ ์ธ์ฆ ๋ฐ ์ธ๊ฐ€ ๊ฒ€์ฆ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๋ณด์•ˆ ์ทจ์•ฝ์ ์„ ํ•ด๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค.

- **UI UX ๊ฐœ์„ **: 'Details' ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ, ์ž‘์—… ์ƒ์„ธ ์ •๋ณด ๋กœ๋“œ ์ค‘์— ์‚ฌ์šฉ์ž๊ฐ€ ๋ช…์‹œ์ ์ธ ๋กœ๋”ฉ ์ƒํƒœ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋„๋ก 'Loading...' ํ…์ŠคํŠธ์™€ ๋น„ํ™œ์„ฑํ™” ์ƒํƒœ๋ฅผ ํ‘œ์‹œํ•˜๋„๋ก ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
- **๊ด€๋ฆฌ์ž์šฉ ๋‹จ๊ฑด ์ž‘์—… ์‚ญ์ œ ๋ฐ ์žฌ์‹œ๋„ API ์ถ”๊ฐ€**
- ํŠน์ • ๋ณ€ํ™˜ ์ž‘์—…์„ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋Š” `DELETE /api/v1/admin/convert/jobs/{jobId}` ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.
Expand Down
52 changes: 52 additions & 0 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Product and Technical Gap Baseline

Status: **Proposed**

Evidence cutoff: 2026-10-01 UTC

Evidence source head: `b20bd4ead4600208960b534b08173ee946a84c7d` on
[clearfolio#659](https://github.com/ContextualWisdomLab/clearfolio/pull/659).

## Goal and bounded context

Clearfolio owns the document-viewer bounded context: authenticated document conversion status,
artifact preview, and administrator recovery operations. Product-domain truth remains in Clearfolio.
Identity claims enter through the tenant-access anti-corruption layer; document extraction and
organization control-plane responsibilities remain external contracts.

## Authoritative artifacts

| Concern | Current evidence | Status |
| --- | --- | --- |
| PRD | `docs/prd-integrated-document-viewer-platform.md` | Current |
| TRD | `docs/trd-integrated-document-viewer-platform.md` | Current |
| Architecture and Context Map | `ARCHITECTURE.md`, `docs/architecture.md` | Current |
| UML and interaction flows | `docs/diagrams/README.md` and bounded flow diagrams | Current |
| Authentication model | `docs/security/2026-07-02-auth-tenant-model.md` | Current |
| ERD | No canonical ERD is published in this repository | Gap |
| Change history | `CHANGELOG.md` | Current |

## Context Map

| Relationship | Contract boundary | Direction |
| --- | --- | --- |
| Identity provider to Clearfolio | Tenant claims and explicit permissions | Upstream to ACL |
| Conversion storage to Clearfolio | Repository interfaces and artifact identifiers | Upstream to ACL |
| Clearfolio to browser | Versioned HTTP responses and signed artifact links | Product API |
| Organization CI to Clearfolio | Reusable security and review workflows | Conformance only |

## Gap and action register

| Gap | Exact evidence | Action | Status |
| --- | --- | --- | --- |
| Administrator endpoints lacked an explicit operation permission | PR #659 source and tests | Require `ADMIN_OPERATE` through `TenantAccessService` | Implemented; exact-head acceptance pending |
| Jackson 2.22.1 is affected by five September 2026 advisories | Security run `36792153106`, job `110147365860` | Pin Jackson BOM and databind to 2.22.3 and guard the POM version | Contract restored at `b20bd4eaโ€ฆ`; exact-head Security Scan pending |
| Canonical ERD is absent | Repository documentation inventory at the evidence head | Publish the persisted conversion-job and tenant ownership model without inventing storage not present in code | Proposed |
| Current successor invalidated predecessor-head CodeQL evidence | PR head advanced after restoring deleted contracts | Require fresh exact-head CodeQL plus independent review | Pending |

## Acceptance rule

A row becomes complete only when its implementation, regression contract, documentation, and
exact-head hosted checks are green. Draft-gated, queued, skipped, stale-head, or predecessor-head
results are not acceptance evidence. This baseline must be updated whenever the PRD, TRD,
Context Map, persistence model, or a listed Gap changes.
5 changes: 4 additions & 1 deletion docs/security/2026-07-02-auth-tenant-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ to the identity provider or gateway, not the viewer service.
| `viewer_user` | `job:create`, `job:read`, `viewer:read`, `artifact-link:create`, `artifact:read` |
| `workflow_client` | `job:create`, `job:read`, `viewer:read` |
| `operator` | `job:read`, `job:retry`, `artifact-link:revoke`, `audit:read` |
| `tenant_admin` | `job:read`, `artifact-link:revoke`, `audit:read`, `tenant:configure` |
| `tenant_admin` | `job:read`, `artifact-link:revoke`, `audit:read`, `tenant:configure`, `admin:operate` |
| `buyer_reviewer` | `job:read`, `viewer:read`, `analytics:read`, `audit:read` in a demo or diligence tenant |

Server-side authorization must check both permission and tenant ownership. A
Expand Down Expand Up @@ -147,6 +147,9 @@ unauthorized action, depending on route semantics.
| `GET /viewer/{docId}` | none for HTML shell | Shell does not inspect job existence; protected JSON APIs decide state. |
| `POST /api/v1/viewer/{docId}/artifact-links` | `artifact-link:create` | Same tenant and succeeded job. |
| `GET /artifacts/{docId}.pdf` | valid signed artifact token | Signed token scope/document/tenant/current checksum/issuance/revocation must match; zero or one Range; record read audit. |
| `GET /api/v1/admin/convert/jobs` | `admin:operate` | Gateway role mapping must grant this permission to `tenant_admin`; the service validates the signed permission claim before listing global jobs. |
| `DELETE /api/v1/admin/convert/jobs/{jobId}` | `admin:operate` | Same grant contract; deny before deletion when absent. |
| `POST /api/v1/admin/convert/jobs/{jobId}/retry` | `admin:operate` | Same grant contract; deny before retry when absent. |
| `GET /api/v1/analytics/kpi-snapshot` | `analytics:read` | Tenant-scoped aggregate by default. |

## Current Branch Implementation Status
Expand Down
12 changes: 5 additions & 7 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,10 @@
which contains the July 2026 HTTP, HTTP/2, MQTT, compression, and
parser-boundary hardening release. -->
<netty.version>4.1.136.Final</netty.version>
<!-- Security override: pull the patched Jackson line for GHSA-5jmj-h7xm-6q6v
/ CVE-2026-54515 case-insensitive deserialization bypasses. The root
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. -->
<jackson-bom.version>2.22.1</jackson-bom.version>
<!-- Security override: Jackson 2.22.3 is the first 2.22.x line fixing
CVE-2026-68497, CVE-2026-91776, CVE-2026-91777, CVE-2026-19032,
CVE-2026-83557, and retains the CVE-2026-54515 fix. -->
<jackson-bom.version>2.22.3</jackson-bom.version>
<!-- Security override: GHSA-jhq6-gfmj-v8fx fixes logback-core in 1.5.35. -->
<logback.version>1.5.35</logback.version>
<!-- Security override: CVE-2025-68161 / CVE-2026-34477 / CVE-2026-34478 / CVE-2026-34480. -->
Expand All @@ -59,7 +57,7 @@
-->
<dependencyManagement>
<dependencies>
<!-- GHSA-5jmj-h7xm-6q6v / CVE-2026-54515: jackson-databind line -->
<!-- September 2026 jackson-databind advisory remediation line -->
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ public final class TenantPermissions {
*/
public static final String ANALYTICS_READ = "analytics:read";

/**
* Permission required for admin operations.
*/
public static final String ADMIN_OPERATE = "admin:operate";

private TenantPermissions() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
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.auth.TenantPermissions;
import com.clearfolio.viewer.model.ConversionJob;
import com.clearfolio.viewer.service.DocumentConversionService;
import com.clearfolio.viewer.service.RetryDeadLetterResult;
Expand All @@ -27,23 +31,34 @@ public class AdminController {

private final DocumentConversionService conversionService;

/**
* Validates required authentication headers and properties.
*/
private final TenantAccessService tenantAccessService;

/**
* Creates a controller for admin operations.
*
* @param conversionService conversion service
* @param tenantAccessService tenant access service
*/
public AdminController(DocumentConversionService conversionService) {
public AdminController(final DocumentConversionService conversionService, final TenantAccessService tenantAccessService) {
this.conversionService = conversionService;
this.tenantAccessService = tenantAccessService;
}

/**
* Retrieves all conversion jobs, optionally filtered by dead-letter status.
*
* @param deadLettered optional filter for dead-lettered jobs
* @param headers request headers
* @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, TenantPermissions.ADMIN_OPERATE);
Iterable<ConversionJob> allJobs = conversionService.getAllJobs();

if (deadLettered == null) {
Expand All @@ -63,10 +78,14 @@ public AdminJobListResponse getAllJobs(@RequestParam(required = false) Boolean d
* Deletes a conversion job.
*
* @param jobId conversion job identifier
* @param headers request headers
* @return no content on success
*/
@DeleteMapping("/api/v1/admin/convert/jobs/{jobId}")
public ResponseEntity<Void> deleteJob(@PathVariable UUID jobId) {
public ResponseEntity<Void> deleteJob(
@PathVariable final UUID jobId,
@RequestHeader final HttpHeaders headers) {
tenantAccessService.require(headers, TenantPermissions.ADMIN_OPERATE);
conversionService.deleteJob(jobId);
return ResponseEntity.noContent().build();
}
Expand All @@ -75,10 +94,14 @@ public ResponseEntity<Void> deleteJob(@PathVariable UUID jobId) {
* Retries a dead-lettered conversion job.
*
* @param jobId conversion job identifier
* @param headers request headers
* @return accepted response on success
*/
@PostMapping("/api/v1/admin/convert/jobs/{jobId}/retry")
public ResponseEntity<Void> retryDeadLettered(@PathVariable UUID jobId) {
public ResponseEntity<Void> retryDeadLettered(
@PathVariable final UUID jobId,
@RequestHeader final HttpHeaders headers) {
tenantAccessService.require(headers, TenantPermissions.ADMIN_OPERATE);
RetryDeadLetterResult result = conversionService.retryDeadLettered(jobId, "admin");
if (result == RetryDeadLetterResult.NOT_FOUND) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "job not found");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.clearfolio.viewer.auth;

import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

import org.junit.jupiter.api.Test;

class TenantPermissionContractTest {

@Test
void tenantAdminRoleCarriesAdminOperatePermission() throws IOException {
String contract = Files.readString(
Path.of("docs/security/2026-07-02-auth-tenant-model.md"));
String tenantAdminRow = contract.lines()
.filter(line -> line.startsWith("| `tenant_admin` |"))
.findFirst()
.orElseThrow();

assertTrue(
tenantAdminRow.contains("`" + TenantPermissions.ADMIN_OPERATE + "`"),
"tenant_admin must receive the permission enforced by AdminController");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@ void pomPinsPatchedNettyLineForReactiveHttpServing() throws Exception {
);
}

@Test
void pomPinsJacksonLinePastSeptember2026DatabindAdvisories() 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 is the first 2.22.x release that fixes CVE-2026-68497, "
+ "CVE-2026-91776, CVE-2026-91777, CVE-2026-19032, and CVE-2026-83557"
);
}

@Test
void mavenVerifyGeneratesWarningFreePublicApiJavadocs() throws Exception {
Document document = parsedPom();
Expand Down
Loading
Loading