Skip to content

🛡️ Sentinel: [CRITICAL] AdminController 권한 우회 취약점 패치 - #210

Closed
seonghobae wants to merge 1 commit into
mainfrom
sentinel-secure-admin-endpoints-4894898224104540289
Closed

🛡️ Sentinel: [CRITICAL] AdminController 권한 우회 취약점 패치#210
seonghobae wants to merge 1 commit into
mainfrom
sentinel-secure-admin-endpoints-4894898224104540289

Conversation

@seonghobae

@seonghobae seonghobae commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Superseded by #172. This branch also collapses all privileges into a single admin:access permission, whereas #172 preserves least privilege with separate read and write capabilities.

AdminController의 관리자 전용 엔드포인트(작업 목록, 삭제, 재시도)에 인증/인가가 누락되어 있던 취약점을 수정했습니다. 이제 모든 요청은 `TenantAccessService`를 통해 `admin:access` 권한을 가진 사용자만 접근할 수 있도록 강제됩니다. 또한 이에 맞게 단위 테스트를 업데이트하고, 발견된 취약점과 개선 사항을 `.jules/sentinel.md` 저널에 기록했습니다.
@google-labs-jules

Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

Copilot AI review requested due to automatic review settings July 26, 2026 21:06
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@seonghobae, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 21 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: dec19161-993d-4429-a38b-fba454db98b5

📥 Commits

Reviewing files that changed from the base of the PR and between ae0bc74 and f74045f.

📒 Files selected for processing (4)
  • .jules/sentinel.md
  • src/main/java/com/clearfolio/viewer/auth/TenantPermissions.java
  • src/main/java/com/clearfolio/viewer/controller/AdminController.java
  • src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sentinel-secure-admin-endpoints-4894898224104540289

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR tightens security around the admin conversion-job endpoints by introducing an explicit admin:access permission and requiring tenant-claim headers to access AdminController. It aligns admin endpoints with the existing tenant-claims model used elsewhere in the viewer service.

Changes:

  • Added TenantPermissions.ADMIN_ACCESS and enforced it via TenantAccessService.require() on all AdminController endpoints.
  • Updated AdminControllerTest to construct the controller with TenantAccessService and send tenant-claim headers.
  • Recorded the vulnerability and prevention guidance in .jules/sentinel.md.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/main/java/com/clearfolio/viewer/controller/AdminController.java Adds ADMIN access enforcement to admin endpoints (but needs tenant scoping / operator attribution fixes).
src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java Updates tests to include tenant-claim headers and mock TenantAccessService (but should assert auth is actually checked).
src/main/java/com/clearfolio/viewer/auth/TenantPermissions.java Introduces the new ADMIN_ACCESS permission constant.
.jules/sentinel.md Documents the admin authorization-bypass vulnerability and prevention guidance.
Comments suppressed due to low confidence (4)

src/main/java/com/clearfolio/viewer/controller/AdminController.java:75

  • getAllJobs() enforces ADMIN_ACCESS but still returns repository.findAll() results unscoped to the caller’s tenant. Since TenantAccessService.require() returns a TenantContext, this endpoint should filter by tenantId (consistent with tenant isolation elsewhere) rather than allowing an admin from one tenant to enumerate other tenants’ jobs.

This issue also appears in the following locations of the same file:

  • line 85
  • line 101
    public AdminJobListResponse getAllJobs(
            @RequestParam(required = false) final Boolean deadLettered,
            @RequestHeader final HttpHeaders headers) {
        tenantAccessSvc.require(headers, TenantPermissions.ADMIN_ACCESS);
        Iterable<ConversionJob> allJobs = conversionSvc.getAllJobs();

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

        List<ConversionJob> filtered = new ArrayList<>();
        for (ConversionJob job : allJobs) {
            if (job.isDeadLettered() == deadLettered) {
                filtered.add(job);
            }
        }
        return AdminJobListResponse.from(filtered);
    }

src/main/java/com/clearfolio/viewer/controller/AdminController.java:90

  • deleteJob() currently calls conversionSvc.deleteJob(jobId) directly, which bypasses the tenant-aware deleteJob(jobId, TenantContext) safeguard used on non-admin endpoints. If ADMIN_ACCESS is a per-tenant permission, this lets an admin from one tenant delete jobs belonging to another tenant.
    public ResponseEntity<Void> deleteJob(
            @PathVariable final UUID jobId,
            @RequestHeader final HttpHeaders headers) {
        tenantAccessSvc.require(headers, TenantPermissions.ADMIN_ACCESS);
        conversionSvc.deleteJob(jobId);
        return ResponseEntity.noContent().build();

src/main/java/com/clearfolio/viewer/controller/AdminController.java:107

  • retryDeadLettered() ignores the TenantContext returned by TenantAccessService.require() and hard-codes operatorId to "admin". This makes the audit/operator attribution incorrect and also skips the tenant-boundary check pattern used elsewhere (requireSameTenant).
    public ResponseEntity<Void> retryDeadLettered(
            @PathVariable final UUID jobId,
            @RequestHeader final HttpHeaders headers) {
        tenantAccessSvc.require(headers, TenantPermissions.ADMIN_ACCESS);
        RetryDeadLetterResult result =
                conversionSvc.retryDeadLettered(jobId, "admin");
        if (result == RetryDeadLetterResult.NOT_FOUND) {

src/test/java/com/clearfolio/viewer/controller/AdminControllerTest.java:41

  • The tests stub TenantAccessService.require(), but they don't assert that the controller actually calls it. As written, the tests would still pass if the access-control call were accidentally removed, so they don't meaningfully cover the new security behavior (coverage gate expects new behavior to be tested, not just code executed).
    @BeforeEach
    void setUp() {
        conversionService = mock(DocumentConversionService.class);
        tenantAccessService = mock(TenantAccessService.class);
        when(tenantAccessService.require(any(), eq(TenantPermissions.ADMIN_ACCESS)))
                .thenReturn(new TenantContext("admin", "admin", Set.of(TenantPermissions.ADMIN_ACCESS)));

        controller = new AdminController(conversionService, tenantAccessService);
        webTestClient = WebTestClient.bindToController(controller)
                .controllerAdvice(new ApiExceptionHandler())
                .build();
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +32 to +35
/** Conversion service. */
private final DocumentConversionService conversionSvc;
/** Tenant access service. */
private final TenantAccessService tenantAccessSvc;
@seonghobae seonghobae closed this Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants