Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
286042b
๐Ÿ›ก๏ธ Sentinel: [CRITICAL] AdminController ์ธ์ฆ ๋ฐ ์ธ๊ฐ€ ์ถ”๊ฐ€
seonghobae Sep 28, 2026
83df09b
๐Ÿ›ก๏ธ Sentinel: [CRITICAL] AdminController ์ธ์ฆ ๋ฐ ์ธ๊ฐ€ ์ถ”๊ฐ€
seonghobae Sep 29, 2026
98dec5a
๐Ÿ›ก๏ธ Sentinel: [CRITICAL] AdminController ์ธ์ฆ ๋ฐ ์ธ๊ฐ€ ์ถ”๊ฐ€
seonghobae Sep 29, 2026
2412daa
๐Ÿ›ก๏ธ Sentinel: [CRITICAL] AdminController ์ธ์ฆ ๋ฐ ์ธ๊ฐ€ ์ถ”๊ฐ€
seonghobae Sep 29, 2026
3bb6a05
๐Ÿ›ก๏ธ Sentinel: [CRITICAL] AdminController ์ธ์ฆ ๋ฐ ์ธ๊ฐ€ ์ถ”๊ฐ€
seonghobae Sep 29, 2026
c086f49
๐Ÿ›ก๏ธ Sentinel: [CRITICAL] AdminController ์ธ์ฆ ๋ฐ ์ธ๊ฐ€ ์ถ”๊ฐ€
seonghobae Sep 30, 2026
7d5ad78
๐Ÿ›ก๏ธ Sentinel: [CRITICAL] AdminController ์ธ์ฆ ๋ฐ ์ธ๊ฐ€ ์ถ”๊ฐ€
seonghobae Sep 30, 2026
c66703d
๐Ÿ›ก๏ธ Sentinel: [CRITICAL] jackson-bom ์ทจ์•ฝ์  ์—…๋ฐ์ดํŠธ
seonghobae Sep 30, 2026
df94480
๐Ÿ›ก๏ธ Sentinel: [CRITICAL] AdminController ์ธ์ฆ ๋ฐ ์ธ๊ฐ€ ์ถ”๊ฐ€
seonghobae Sep 30, 2026
9964fe1
fix: enforce tenant scope for admin job operations
seonghobae Sep 30, 2026
07b351d
๐Ÿ›ก๏ธ Sentinel: [CRITICAL] AdminController ์ธ์ฆ ๋ฐ ์ธ๊ฐ€ ์ถ”๊ฐ€
seonghobae Sep 30, 2026
01bc25d
fix: restore tenant scope after stale writer update
seonghobae Sep 30, 2026
173e238
๐Ÿ›ก๏ธ Sentinel: [CRITICAL] AdminController ์ธ์ฆ ๋ฐ ์ธ๊ฐ€ ๋ˆ„๋ฝ ์ˆ˜์ •
seonghobae Sep 30, 2026
86dd279
test: normalize admin security test imports
seonghobae Sep 30, 2026
baeaea1
๐Ÿ›ก๏ธ Sentinel: [CRITICAL] AdminController ์ธ์ฆ ๋ฐ ์ธ๊ฐ€ ๋ˆ„๋ฝ ์ˆ˜์ •
seonghobae Sep 30, 2026
8b7e294
๐Ÿ›ก๏ธ Sentinel: [CRITICAL] AdminController ์ธ์ฆ ๋ฐ ์ธ๊ฐ€ ๋ˆ„๋ฝ ์ˆ˜์ •
seonghobae Sep 30, 2026
6926d9e
trigger CI: Retry due to CodeQL / OpenCode async scan pattern
seonghobae Sep 30, 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
10 changes: 10 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,13 @@
**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-28 - [CRITICAL] AdminController ์ธ์ฆ ๋ฐ ์ธ๊ฐ€ ์ถ”๊ฐ€
**Vulnerability:** The endpoints in `AdminController` (`/api/v1/admin/convert/jobs`, `/api/v1/admin/convert/jobs/{jobId}`, and `/api/v1/admin/convert/jobs/{jobId}/retry`) did not require authentication or authorization checks.
**Learning:** Security measures must be applied to administrative endpoints to prevent unauthorized access and data modification, even if the API seems to be used only internally or by privileged tools.
**Prevention:** Ensure all newly created controllers and endpoints implement `TenantAccessService.require` appropriately with clear permission mappings such as `ADMIN_READ` and `ADMIN_WRITE`.

## 2026-09-30 - [CRITICAL] Update jackson-bom to address multiple CVEs
**Vulnerability:** Trivy reported `CVE-2026-68497`, `CVE-2026-19032`, and `CVE-2026-83557` targeting `com.fasterxml.jackson.core:jackson-databind` in the dependency tree.
**Learning:** Legacy dependencies like `jackson-bom` should be aggressively monitored and updated to prevent deserialization or related attacks.
**Prevention:** Always bump `jackson-bom.version` in the root `pom.xml` dependency management section when critical CVEs emerge.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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. -->
<jackson-bom.version>2.22.1</jackson-bom.version>
<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 Down
11 changes: 11 additions & 0 deletions src/main/java/com/clearfolio/viewer/auth/TenantPermissions.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ public final class TenantPermissions {
*/
public static final String ANALYTICS_READ = "analytics:read";


/**
* Permission required to read administrative data.
*/
public static final String ADMIN_READ = "admin:read";

/**
* Permission required to perform administrative actions.
*/
public static final String ADMIN_WRITE = "admin:write";

private TenantPermissions() {
}
}
55 changes: 40 additions & 15 deletions src/main/java/com/clearfolio/viewer/controller/AdminController.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.http.HttpHeaders;
import com.clearfolio.viewer.auth.TenantAccessService;
import com.clearfolio.viewer.auth.TenantContext;
import com.clearfolio.viewer.auth.TenantPermissions;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

Expand All @@ -26,33 +31,38 @@
public class AdminController {

private final DocumentConversionService conversionService;
private final TenantAccessService tenantAccessService;

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

/**
* 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) {
Iterable<ConversionJob> allJobs = conversionService.getAllJobs();

if (deadLettered == null) {
return AdminJobListResponse.from(allJobs);
}

List<ConversionJob> filtered = new ArrayList<>();
for (ConversionJob job : allJobs) {
if (job.isDeadLettered() == deadLettered) {
public AdminJobListResponse getAllJobs(
@RequestParam(required = false) final Boolean deadLettered,
@RequestHeader final HttpHeaders headers) {
final TenantContext tenantContext =
tenantAccessService.require(headers, TenantPermissions.ADMIN_READ);
final List<ConversionJob> filtered = new ArrayList<>();
for (final ConversionJob job : conversionService.getAllJobs()) {
if (job.belongsToTenant(tenantContext.tenantId())
&& (deadLettered == null || job.isDeadLettered() == deadLettered)) {
filtered.add(job);
}
}
Expand All @@ -63,23 +73,38 @@ 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<Void> deleteJob(@PathVariable UUID jobId) {
conversionService.deleteJob(jobId);
public ResponseEntity<Void> deleteJob(
@PathVariable final UUID jobId,
@RequestHeader final HttpHeaders headers) {
final TenantContext tenantContext =
tenantAccessService.require(headers, TenantPermissions.ADMIN_WRITE);
if (!conversionService.deleteJob(jobId, tenantContext)) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "job not found");
}
return ResponseEntity.noContent().build();
}

/**
* 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<Void> retryDeadLettered(@PathVariable UUID jobId) {
RetryDeadLetterResult result = conversionService.retryDeadLettered(jobId, "admin");
public ResponseEntity<Void> retryDeadLettered(
@PathVariable final UUID jobId,
@RequestHeader final HttpHeaders headers) {
final TenantContext tenantContext =
tenantAccessService.require(headers, TenantPermissions.ADMIN_WRITE);
final ConversionJob job = conversionService.getJob(jobId)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "job not found"));
tenantAccessService.requireSameTenant(tenantContext, job);
final RetryDeadLetterResult result = conversionService.retryDeadLettered(jobId, "admin");
if (result == RetryDeadLetterResult.NOT_FOUND) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "job not found");
}
Expand Down
Loading
Loading