🛡️ Sentinel: [CRITICAL] AdminController 권한 우회 취약점 패치 - #210
Conversation
AdminController의 관리자 전용 엔드포인트(작업 목록, 삭제, 재시도)에 인증/인가가 누락되어 있던 취약점을 수정했습니다. 이제 모든 요청은 `TenantAccessService`를 통해 `admin:access` 권한을 가진 사용자만 접근할 수 있도록 강제됩니다. 또한 이에 맞게 단위 테스트를 업데이트하고, 발견된 취약점과 개선 사항을 `.jules/sentinel.md` 저널에 기록했습니다.
|
👋 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 New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
|
Warning Review limit reached
Next review available in: 21 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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_ACCESSand enforced it viaTenantAccessService.require()on allAdminControllerendpoints. - Updated
AdminControllerTestto construct the controller withTenantAccessServiceand 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.
| /** Conversion service. */ | ||
| private final DocumentConversionService conversionSvc; | ||
| /** Tenant access service. */ | ||
| private final TenantAccessService tenantAccessSvc; |
Superseded by #172. This branch also collapses all privileges into a single
admin:accesspermission, whereas #172 preserves least privilege with separate read and write capabilities.