Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ public Mono<ResponseEntity<byte[]>> getPdf(
@RequestParam(value = ArtifactLinkService.ARTIFACT_TOKEN_PARAM, required = false) String queryToken,
@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authorizationHeader,
@RequestHeader(value = "X-Request-Id", required = false) String traceId) {
String token = ArtifactLinkService.resolveToken(queryToken, authorizationHeader);
if (token == null) {
return Mono.just(ArtifactHttpResponse.tokenFailure(HttpStatus.UNAUTHORIZED));
}

Comment on lines +139 to +143

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

객체 조회 전에 토큰을 인증하십시오.

resolveToken은 비어 있지 않은 쿼리 토큰을 그대로 반환합니다. 따라서 artifactToken=opaque-present-token 요청은 conversionService.getJob(docId) 및 artifactStore.getPdf(docId)에 도달합니다. 공격자는 유효하지 않은 토큰으로 문서 존재 상태를 관찰할 수 있습니다.

객체 조회 전에 서명, 만료, 범위, docId 바인딩 및 취소 상태를 검증하는 단계를 추가하십시오. 검증되지 않은 토큰은 조회 전에 토큰 실패 응답을 반환해야 합니다.

  • src/main/java/com/clearfolio/viewer/controller/ArtifactController.java#L139-L143: resolveToken 뒤에 사전 인증을 추가하고, 인증 성공 후에만 job 및 artifact 조회를 실행하십시오.
  • src/test/java/com/clearfolio/viewer/controller/ArtifactControllerTokenPreconditionTest.java#L25-L73: 비어 있지 않은 잘못된 쿼리 토큰 및 Bearer 토큰도 401과 collaborator 미호출을 보장하는 테스트를 추가하십시오.
  • src/test/java/com/clearfolio/viewer/controller/ArtifactControllerTest.java#L56-L56: 사전 인증을 통과하는 토큰으로 job-missing 404 동작을 검증하십시오.
  • src/test/java/com/clearfolio/viewer/controller/ArtifactControllerTest.java#L75-L75: 사전 인증을 통과하는 토큰으로 non-succeeded job 404 동작을 검증하십시오.
  • src/test/java/com/clearfolio/viewer/controller/ArtifactControllerTest.java#L87-L87: 사전 인증을 통과하는 토큰으로 artifact-missing 404 동작을 검증하십시오.
📍 Affects 3 files
  • src/main/java/com/clearfolio/viewer/controller/ArtifactController.java#L139-L143 (this comment)
  • src/test/java/com/clearfolio/viewer/controller/ArtifactControllerTokenPreconditionTest.java#L25-L73
  • src/test/java/com/clearfolio/viewer/controller/ArtifactControllerTest.java#L56-L56
  • src/test/java/com/clearfolio/viewer/controller/ArtifactControllerTest.java#L75-L75
  • src/test/java/com/clearfolio/viewer/controller/ArtifactControllerTest.java#L87-L87
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/clearfolio/viewer/controller/ArtifactController.java`
around lines 139 - 143,
src/main/java/com/clearfolio/viewer/controller/ArtifactController.java:139-143에서
resolveToken 이후 서명·만료·범위·docId 바인딩·취소 상태를 검증하고, 성공한 경우에만
conversionService.getJob 및 artifactStore.getPdf를 호출하십시오.
src/test/java/com/clearfolio/viewer/controller/ArtifactControllerTokenPreconditionTest.java:25-73에는
잘못된 쿼리 및 Bearer 토큰이 401을 반환하고 collaborator를 호출하지 않는 테스트를 추가하십시오.
src/test/java/com/clearfolio/viewer/controller/ArtifactControllerTest.java:56,
75, 87의 job-missing, non-succeeded job, artifact-missing 테스트는 사전 인증을 통과하는 토큰을
사용하도록 갱신하십시오.

Source: Coding guidelines

Optional<ConversionJob> job = conversionService.getJob(docId);
if (job.isEmpty() || job.get().getStatus() != ConversionJobStatus.SUCCEEDED) {
return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND).build());
Expand All @@ -147,7 +152,6 @@ public Mono<ResponseEntity<byte[]>> getPdf(
}

byte[] pdfBytes = stored.get();
String token = ArtifactLinkService.resolveToken(queryToken, authorizationHeader);
ArtifactTokenClaims claims;
try {
claims = artifactLinkService.verifyReadToken(docId, job.get(), pdfBytes, token);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ void returnsNotFoundWhenJobMissing() {
when(conversionService.getJob(docId)).thenReturn(Optional.empty());

webTestClient.get()
.uri("/artifacts/{docId}.pdf", docId)
.uri("/artifacts/{docId}.pdf?artifactToken=opaque-present-token", docId)
.exchange()
.expectStatus().isNotFound();
}
Expand All @@ -72,7 +72,7 @@ void returnsNotFoundWhenJobIsNotSucceeded() {
when(conversionService.getJob(docId)).thenReturn(Optional.of(job));

webTestClient.get()
.uri("/artifacts/{docId}.pdf", docId)
.uri("/artifacts/{docId}.pdf?artifactToken=opaque-present-token", docId)
.exchange()
.expectStatus().isNotFound();
}
Expand All @@ -84,7 +84,7 @@ void returnsNotFoundWhenArtifactIsMissing() {
when(conversionService.getJob(docId)).thenReturn(Optional.of(job));

webTestClient.get()
.uri("/artifacts/{docId}.pdf", docId)
.uri("/artifacts/{docId}.pdf?artifactToken=opaque-present-token", docId)
.exchange()
.expectStatus().isNotFound();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.clearfolio.viewer.controller;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;

import java.util.UUID;

import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import com.clearfolio.viewer.artifact.ArtifactLinkService;
import com.clearfolio.viewer.artifact.ArtifactStore;
import com.clearfolio.viewer.auth.TenantAccessService;
import com.clearfolio.viewer.service.DocumentConversionService;

/**
* Proves that the public artifact route authenticates token presence before
* reading conversion-job or artifact state that could become an existence oracle.
*/
class ArtifactControllerTokenPreconditionTest {

@Test
void missingArtifactTokenIsRejectedBeforeSensitiveObjectLookup() {
DocumentConversionService conversionService = mock(DocumentConversionService.class);
ArtifactStore artifactStore = mock(ArtifactStore.class);
ArtifactLinkService artifactLinkService = mock(ArtifactLinkService.class);
ArtifactController controller = new ArtifactController(
conversionService,
artifactStore,
artifactLinkService,
mock(TenantAccessService.class)
);

ResponseEntity<byte[]> response = controller.getPdf(
UUID.randomUUID(),
null,
null,
null,
null
).block();

assertNotNull(response);
assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
verifyNoInteractions(conversionService, artifactStore, artifactLinkService);
}

@Test
void unsupportedAuthorizationSchemeIsRejectedBeforeSensitiveObjectLookup() {
DocumentConversionService conversionService = mock(DocumentConversionService.class);
ArtifactStore artifactStore = mock(ArtifactStore.class);
ArtifactLinkService artifactLinkService = mock(ArtifactLinkService.class);
ArtifactController controller = new ArtifactController(
conversionService,
artifactStore,
artifactLinkService,
mock(TenantAccessService.class)
);

ResponseEntity<byte[]> response = controller.getPdf(
UUID.randomUUID(),
null,
" ",
"Basic opaque-credential",
null
).block();

assertNotNull(response);
assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode());
verifyNoInteractions(conversionService, artifactStore, artifactLinkService);
}
}
Loading