Skip to content

[FEAT] 계약서 내보내기#89

Closed
minnieming wants to merge 10 commits intodevelopfrom
feat/contract-export
Closed

[FEAT] 계약서 내보내기#89
minnieming wants to merge 10 commits intodevelopfrom
feat/contract-export

Conversation

@minnieming
Copy link
Contributor

@minnieming minnieming commented Aug 17, 2025

🚀 관련 이슈

🔑 주요 변경사항

✔️ 체크 리스트

  • Merge 하려는 브랜치가 올바른가? (main branch에 실수로 PR 생성 금지)
  • Merge 하려는 PR 및 Commit들을 로컬에서 실행했을 때 에러가 발생하지 않았는가?
  • 라벨을 등록했는가?
  • 리뷰어를 지정했는가?

📢 To Reviewers

📸 스크린샷 or 실행영상

↗️ 개선 사항

Summary by CodeRabbit

  • New Features
    • 최종 계약서 PDF 생성, 화면 표시/다운로드, 이메일 발송 기능을 추가했습니다.
    • 전자서명 이미지 업로드·저장(암호화) 및 서명 유형별 처리(TAX/PRIORITY/최종 서명)를 지원합니다.
    • 비밀번호 설정/입력으로 최종 계약서 PDF를 안전하게 보호할 수 있습니다.
    • 계약 당사자 정보, 부동산 상세, 임대 조건, 일정 등 확장된 메타데이터를 반영합니다.
    • 금액 표기 가독성을 개선해 원 단위 축약 및 한글 숫자 표기를 지원합니다.

@minnieming minnieming requested a review from MeongW August 17, 2025 16:06
@minnieming minnieming self-assigned this Aug 17, 2025
@minnieming minnieming added the ✨ feature 새로운 기능 요청 label Aug 17, 2025
@minnieming minnieming linked an issue Aug 17, 2025 that may be closed by this pull request
3 tasks
@coderabbitai
Copy link

coderabbitai bot commented Aug 17, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

최종 계약서 내보내기/서명/암호화/이메일 전송까지의 신규 워크플로우를 컨트롤러·서비스·매퍼 전반에 추가/대체했습니다. 다수의 DTO/VO/enum이 신설되었고, Mapper XML이 대폭 갱신되었습니다. AES-GCM 기반 파일 암호화·해시 및 Multipart 변환, 숫자 포맷 유틸이 추가되었습니다.

Changes

Cohort / File(s) Summary
Controllers
src/main/java/.../contract/controller/ContractController.java, .../ContractControllerImpl.java
최종 PDF 생성/조회/다운로드/이메일 전송, 서명 저장, 최종 계약 저장 API 엔드포인트 추가 및 구현. Multipart, RequestPart, HttpServletResponse 처리 추가.
Service Layer
.../service/ContractService.java, .../service/ContractServiceImpl.java
최종 PDF 생성, 암호화 서명 저장, 비밀번호 기반 PDF 조회/스트리밍/이메일 발송 로직 추가. Redis 단계 상태, 금액 포맷, AI 서버 연동, S3 업로드/해시, 예외 처리 확장.
DTOs
.../dto/ContractPasswordDTO.java, .../dto/DBFinalContractDTO.java, .../dto/FinalContractDTO.java, .../dto/FindContractDTO.java, .../dto/LegalityRequestDTO.java, .../dto/PrioritySignatureDTO.java, .../dto/SaveFinalContractDTO.java, .../dto/SaveSignatureDTO.java
최종 계약/서명/비밀번호/조회/AI 매핑용 DTO 다수 추가·확장. FinalContractDTO에 파일기반 서명 필드 및 상세 속성 대거 추가. LegalityRequestDTO는 Lombok 어노테이션 정리.
VOs
.../vo/ElectronicSignature.java, .../vo/FinalContract.java
전자서명, 최종 계약 엔티티용 VO 추가.
Enums
.../enums/SignedType.java, .../precontract/enums/ContractDuration.java
서명 타입 enum 신설. ContractDuration에 years 값 추가 및 접근자 제공.
Mapper + SQL
.../mapper/ContractMapper.java, src/main/resources/.../ContractMapper.xml
최종 계약/서명 플로우용 쿼리 세트 신설(초기화/조회/업데이트, 전자서명 insert/select, 사용자 birth/email 조회). 구 기존 메서드 제거. ResultMap 추가.
Utilities
.../util/ImgAesCryptoUtil.java, .../util/MultipartFileUtils.java, .../util/NumberFormatUtil.java
AES-GCM 암복호화·해시, Multipart-File/Stream 변환, 한글 금액/숫자 포맷 유틸 추가.
Repository
.../repository/ContractMongoRepository.java
주석 추가(동작 변화 없음).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant Ctrl as ContractController
  participant Svc as ContractService
  participant Map as ContractMapper
  participant S3 as S3
  participant AI as AI Server
  participant R as Redis

  C->>Ctrl: POST /contract/{id}/final_contract
  Ctrl->>Svc: finalContractPDF(contractChatId, userId)
  Svc->>Map: selectFinalContractPDF/selectFinalContract
  Svc->>R: get step/payment state
  Svc->>AI: generate final PDF (payload DTO)
  AI-->>Svc: PDF bytes
  Svc->>S3: upload PDF
  S3-->>Svc: s3Key, hash
  Svc->>Map: updateFinalContract(s3Key, hash)
  Svc-->>Ctrl: MultipartFile (PDF)
  Ctrl-->>C: 200 PDF
Loading
sequenceDiagram
  autonumber
  participant C as Client
  participant Ctrl as ContractController
  participant Svc as ContractService
  participant AES as ImgAesCryptoUtil
  participant S3 as S3
  participant Map as ContractMapper

  C->>Ctrl: POST /contract/{id}/signature (dto + image)
  Ctrl->>Svc: saveSignature(id, userId, dto, img)
  Svc->>AES: encrypt img, sha256
  AES-->>Svc: base64Cipher, hash
  Svc->>S3: upload encrypted image
  S3-->>Svc: s3Key
  Svc->>Map: insertSignature(id, s3Key, hash, signedType, userId)
  Svc-->>Ctrl: Boolean (bothSigned?)
  Ctrl-->>C: 200
Loading
sequenceDiagram
  autonumber
  participant C as Client
  participant Ctrl as ContractController
  participant Svc as ContractService
  participant Map as ContractMapper
  participant Mail as Mailer

  C->>Ctrl: POST /contract/{id}/email (FindContractDTO)
  Ctrl->>Svc: sendContractPDF(id, userId, dto)
  Svc->>Map: selectFinalContract/selectMail
  Svc-->>Svc: build password-protected PDF bytes
  Svc->>Mail: send email with attachment
  Mail-->>Svc: ok
  Svc-->>Ctrl: Void
  Ctrl-->>C: 200
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Assessment against linked issues

Objective Addressed Explanation
계약 전 채팅 로직 구현, API 구현, 리스트 API (#1) 변경점이 계약서/서명/내보내기 워크플로우에 한정됨.
SSE 활용한 채팅 알림 구현 (#1) SSE 관련 추가나 수정 없음.
실시간 전송(WebSocket+STOMP), 데이터 동기화 (#1) 채팅 전송/구독 관련 코드 변경 없음.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
AES-GCM 유틸 추가 및 파일 해시 기능 (.../util/ImgAesCryptoUtil.java) 채팅 모듈 목표와 무관한 암호화 기능.
Multipart/File 변환 유틸 추가 (.../util/MultipartFileUtils.java) 파일 업로드 유틸리티로 채팅 이슈 범위를 벗어남.
한글 금액/숫자 포맷 유틸 추가 (.../util/NumberFormatUtil.java) 포맷팅 유틸로 채팅 요구사항과 관련 없음.
최종 계약/서명/이메일 전송 엔드포인트 대거 추가 (.../contract/controller/*) 채팅 API가 아닌 계약서 내보내기 기능.
ContractDuration enum에 years 필드 추가 (.../precontract/enums/ContractDuration.java) 계약 기간 표현 변경으로 채팅 기능과 관련 없음.

Possibly related PRs

Suggested reviewers

  • MeongW

Poem

새벽 잉크로 찍은 발자국, 사인의 춤을 춘다
바람에 넘긴 계약서, PDF 달빛에 반짝인다 ✨
토끼는 해시를 품고, 열쇠로 밤을 잠근다 🔐
만 원, 억 원 숫자노래—콩콩 뛰는 발뒤꿈치
이제 메일로 훨훨, 내보내기 길 위에 봄꽃 🌸

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e205f6c and 2eff907.

📒 Files selected for processing (23)
  • src/main/java/org/scoula/domain/contract/controller/ContractController.java (2 hunks)
  • src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java (2 hunks)
  • src/main/java/org/scoula/domain/contract/dto/ContractPasswordDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/DBFinalContractDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/FinalContractDTO.java (2 hunks)
  • src/main/java/org/scoula/domain/contract/dto/FindContractDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/LegalityRequestDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/PrioritySignatureDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/SaveFinalContractDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/dto/SaveSignatureDTO.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/enums/SignedType.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/exception/ContractException.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/mapper/ContractMapper.java (2 hunks)
  • src/main/java/org/scoula/domain/contract/repository/ContractMongoRepository.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/service/ContractService.java (2 hunks)
  • src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java (11 hunks)
  • src/main/java/org/scoula/domain/contract/vo/ElectronicSignature.java (1 hunks)
  • src/main/java/org/scoula/domain/contract/vo/FinalContract.java (1 hunks)
  • src/main/java/org/scoula/domain/precontract/enums/ContractDuration.java (1 hunks)
  • src/main/java/org/scoula/global/common/util/ImgAesCryptoUtil.java (1 hunks)
  • src/main/java/org/scoula/global/common/util/MultipartFileUtils.java (1 hunks)
  • src/main/java/org/scoula/global/common/util/NumberFormatUtil.java (1 hunks)
  • src/main/resources/org/scoula/domain/contract/mapper/ContractMapper.xml (1 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/contract-export

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@minnieming minnieming closed this Aug 17, 2025

@Override
@GetMapping("/final_contract")
public ResponseEntity<ApiResponse<MultipartFile>> finalContractPDF(

Check failure

Code scanning / CodeQL

HTTP request type unprotected from CSRF High

Potential CSRF vulnerability due to using an HTTP request type which is not default-protected from CSRF for an apparent
state-changing action
.
Potential CSRF vulnerability due to using an HTTP request type which is not default-protected from CSRF for an apparent
state-changing action
.

Copilot Autofix

AI 6 months ago

To fix this issue, the HTTP method for the finalContractPDF endpoint should be changed from GET to POST (or another "unsafe" method like PUT or PATCH) to ensure that Spring's default CSRF protection applies. This involves changing the @GetMapping("/final_contract") annotation to @PostMapping("/final_contract") on the finalContractPDF method in ContractControllerImpl.java. No changes are needed in the service layer, as the vulnerability is in the controller's HTTP method exposure. If clients are using this endpoint, they will need to update their requests to use POST instead of GET.

Steps:

  • In ContractControllerImpl.java, locate the finalContractPDF method.
  • Change @GetMapping("/final_contract") to @PostMapping("/final_contract").
  • No additional imports or method changes are required.

Suggested changeset 1
src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java
--- a/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java
+++ b/src/main/java/org/scoula/domain/contract/controller/ContractControllerImpl.java
@@ -221,7 +221,7 @@
       //      }
 
       @Override
-      @GetMapping("/final_contract")
+      @PostMapping("/final_contract")
       public ResponseEntity<ApiResponse<MultipartFile>> finalContractPDF(
               @PathVariable Long contractChatId,
               @AuthenticationPrincipal CustomUserDetails userDetails) {
EOF
@@ -221,7 +221,7 @@
// }

@Override
@GetMapping("/final_contract")
@PostMapping("/final_contract")
public ResponseEntity<ApiResponse<MultipartFile>> finalContractPDF(
@PathVariable Long contractChatId,
@AuthenticationPrincipal CustomUserDetails userDetails) {
Copilot is powered by AI and may make mistakes. Always verify output.
restTemplate.exchange(url, HttpMethod.POST, requestEntity, LegalityDTO.class);
LegalityDTO res = response.getBody();
assert res != null;
log.warn("AI 응답 값 확인: {}", res.toString());

Check failure

Code scanning / CodeQL

Log Injection High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI 6 months ago

To fix the log injection vulnerability, we should sanitize the data before logging it, just as is done in the error path. Specifically, before logging the response object (res) in the success path, we should serialize it to a JSON string and remove any newline (\n) or carriage return (\r) characters. This ensures that even if the response contains malicious content, it cannot break the log format or inject additional log entries. The fix should be applied to line 473 in ContractServiceImpl.java. We can reuse the same approach as in the error path: use ObjectMapper to serialize the object, and then replace any newline or carriage return characters with spaces before logging.

No changes are needed in LegalityDTO.java.
We may need to ensure that ObjectMapper is available in the method scope.


Suggested changeset 1
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
--- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
+++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
@@ -470,7 +470,16 @@
                     restTemplate.exchange(url, HttpMethod.POST, requestEntity, LegalityDTO.class);
             LegalityDTO res = response.getBody();
             assert res != null;
-            log.warn("AI 응답 값 확인: {}", res.toString());
+            // Sanitize AI response before logging to prevent log injection
+            String sanitizedResStr;
+            try {
+                ObjectMapper objectMapper = new ObjectMapper();
+                sanitizedResStr = objectMapper.writeValueAsString(res);
+            } catch (Exception ex) {
+                sanitizedResStr = String.valueOf(res);
+            }
+            sanitizedResStr = sanitizedResStr.replaceAll("[\\r\\n]", " ");
+            log.warn("AI 응답 값 확인: {}", sanitizedResStr);
 
             log.warn("AI 응답 헤더 확인: {}", response.getStatusCode());
             if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
EOF
@@ -470,7 +470,16 @@
restTemplate.exchange(url, HttpMethod.POST, requestEntity, LegalityDTO.class);
LegalityDTO res = response.getBody();
assert res != null;
log.warn("AI 응답 값 확인: {}", res.toString());
// Sanitize AI response before logging to prevent log injection
String sanitizedResStr;
try {
ObjectMapper objectMapper = new ObjectMapper();
sanitizedResStr = objectMapper.writeValueAsString(res);
} catch (Exception ex) {
sanitizedResStr = String.valueOf(res);
}
sanitizedResStr = sanitizedResStr.replaceAll("[\\r\\n]", " ");
log.warn("AI 응답 값 확인: {}", sanitizedResStr);

log.warn("AI 응답 헤더 확인: {}", response.getStatusCode());
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
Copilot is powered by AI and may make mistakes. Always verify output.
}
// Remove newlines and carriage returns
responseBodyStr = responseBodyStr.replaceAll("[\\r\\n]", " ");
log.error(responseBodyStr);

Check failure

Code scanning / CodeQL

Log Injection High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI 6 months ago

To fully mitigate log injection, all user-controlled or external data written to logs should be sanitized to remove not only newlines and carriage returns, but also other control characters that could be used to forge log entries or disrupt log parsing. The best way to fix this is to implement a utility method that strips or escapes all non-printable characters from the string before logging. This method should be applied to responseBodyStr before it is passed to the logger. The fix should be made in the getLegality method of ContractServiceImpl.java, specifically in the block where responseBodyStr is logged. If a suitable utility method does not already exist, it should be defined in this file or imported from a well-known library. For this fix, we will define a private static method in the class to sanitize log messages, and use it before logging responseBodyStr.


Suggested changeset 1
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
--- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
+++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
@@ -54,6 +54,16 @@
 
 @Service
 @RequiredArgsConstructor
+
+    /**
+     * Sanitizes a string for safe logging by removing control characters that could be used for log injection.
+     * This includes newlines, carriage returns, and other non-printable characters.
+     */
+    private static String sanitizeForLog(String input) {
+        if (input == null) return null;
+        // Remove all control characters except tab (\\t) if desired
+        return input.replaceAll("[\\p{Cntrl}&&[^\t]]", "");
+    }
 @Log4j2
 public class ContractServiceImpl implements ContractService {
 
@@ -484,8 +494,8 @@
                 } catch (Exception ex) {
                     responseBodyStr = String.valueOf(response.getBody());
                 }
-                // Remove newlines and carriage returns
-                responseBodyStr = responseBodyStr.replaceAll("[\\r\\n]", " ");
+                // Remove control characters to prevent log injection
+                responseBodyStr = sanitizeForLog(responseBodyStr);
                 log.error(responseBodyStr);
                 throw new BusinessException(ContractException.CONTRACT_AI_SERVER_ERROR);
             }
EOF
@@ -54,6 +54,16 @@

@Service
@RequiredArgsConstructor

/**
* Sanitizes a string for safe logging by removing control characters that could be used for log injection.
* This includes newlines, carriage returns, and other non-printable characters.
*/
private static String sanitizeForLog(String input) {
if (input == null) return null;
// Remove all control characters except tab (\\t) if desired
return input.replaceAll("[\\p{Cntrl}&&[^\t]]", "");
}
@Log4j2
public class ContractServiceImpl implements ContractService {

@@ -484,8 +494,8 @@
} catch (Exception ex) {
responseBodyStr = String.valueOf(response.getBody());
}
// Remove newlines and carriage returns
responseBodyStr = responseBodyStr.replaceAll("[\\r\\n]", " ");
// Remove control characters to prevent log injection
responseBodyStr = sanitizeForLog(responseBodyStr);
log.error(responseBodyStr);
throw new BusinessException(ContractException.CONTRACT_AI_SERVER_ERROR);
}
Copilot is powered by AI and may make mistakes. Always verify output.
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
// 응답 바이트를 파일로 저장
byte[] fileBytes = response.getBody();
File tempFile = File.createTempFile("contract_", ".pdf");

Check warning

Code scanning / CodeQL

Local information disclosure in a temporary directory Medium

Local information disclosure vulnerability due to use of file readable by other local users.

Copilot Autofix

AI 6 months ago

To fix the problem, the temporary file should be created with permissions that restrict access to only the owner. The best way to do this is to use Files.createTempFile from the java.nio.file package, which by default creates files with permissions -rw------- on Unix-like systems. This method returns a Path, which can be converted to a File if needed. The code on line 763 should be replaced with a call to Files.createTempFile("contract_", ".pdf"), and the rest of the code should use the resulting Path (or convert it to a File if required by downstream APIs). No change in functionality is required, only the method of file creation.

You will need to:

  • Replace File.createTempFile("contract_", ".pdf") with Files.createTempFile("contract_", ".pdf").
  • Convert the resulting Path to a File if needed (e.g., for MultipartFileUtils.fromFile).
  • Ensure that the import for java.nio.file.Files is present (it already is).
  • No additional dependencies are required.
Suggested changeset 1
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
--- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
+++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
@@ -760,9 +760,9 @@
             if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
                 // 응답 바이트를 파일로 저장
                 byte[] fileBytes = response.getBody();
-                File tempFile = File.createTempFile("contract_", ".pdf");
-                Files.write(tempFile.toPath(), fileBytes);
-                result = MultipartFileUtils.fromFile(tempFile, "contract.pdf", "application/pdf");
+                java.nio.file.Path tempFilePath = Files.createTempFile("contract_", ".pdf");
+                Files.write(tempFilePath, fileBytes);
+                result = MultipartFileUtils.fromFile(tempFilePath.toFile(), "contract.pdf", "application/pdf");
 
                 // s3에 파일 업로드 하기
                 String key = s3Service.uploadFile(result);
EOF
@@ -760,9 +760,9 @@
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
// 응답 바이트를 파일로 저장
byte[] fileBytes = response.getBody();
File tempFile = File.createTempFile("contract_", ".pdf");
Files.write(tempFile.toPath(), fileBytes);
result = MultipartFileUtils.fromFile(tempFile, "contract.pdf", "application/pdf");
java.nio.file.Path tempFilePath = Files.createTempFile("contract_", ".pdf");
Files.write(tempFilePath, fileBytes);
result = MultipartFileUtils.fromFile(tempFilePath.toFile(), "contract.pdf", "application/pdf");

// s3에 파일 업로드 하기
String key = s3Service.uploadFile(result);
Copilot is powered by AI and may make mistakes. Always verify output.
}
// Remove newlines and carriage returns
responseBodyStr = responseBodyStr.replaceAll("[\\r\\n]", " ");
log.error(responseBodyStr);

Check failure

Code scanning / CodeQL

Log Injection High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI 6 months ago

To fully mitigate log injection, the log entry should:

  1. Sanitize the response body string by removing or escaping all control characters that could affect log structure (not just newlines and carriage returns, but also tabs and other non-printable characters).
  2. Clearly mark the user-controlled data in the log entry, so that it cannot be confused with other log entries.
  3. Use a structured logging format, such as including a label or context in the log message.

The best way to fix the problem is to:

  • Further sanitize responseBodyStr by removing all non-printable/control characters (e.g., using a regex to keep only printable characters).
  • Log the value with a clear label, e.g., AI server error response: [sanitized value].
  • Make this change only in the block where the log entry is written (line 785).

No new imports are needed, as the required functionality can be implemented with standard Java methods.


Suggested changeset 1
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
--- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
+++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
@@ -781,8 +781,9 @@
                     responseBodyStr = String.valueOf(response.getBody());
                 }
                 // Remove newlines and carriage returns
-                responseBodyStr = responseBodyStr.replaceAll("[\\r\\n]", " ");
-                log.error(responseBodyStr);
+                // Remove all control characters (including newlines, carriage returns, tabs, etc.)
+                responseBodyStr = responseBodyStr.replaceAll("[\\p{Cntrl}]", " ");
+                log.error("AI server error response: [{}]", responseBodyStr);
                 throw new BusinessException(ContractException.CONTRACT_AI_SERVER_ERROR);
             }
 
EOF
@@ -781,8 +781,9 @@
responseBodyStr = String.valueOf(response.getBody());
}
// Remove newlines and carriage returns
responseBodyStr = responseBodyStr.replaceAll("[\\r\\n]", " ");
log.error(responseBodyStr);
// Remove all control characters (including newlines, carriage returns, tabs, etc.)
responseBodyStr = responseBodyStr.replaceAll("[\\p{Cntrl}]", " ");
log.error("AI server error response: [{}]", responseBodyStr);
throw new BusinessException(ContractException.CONTRACT_AI_SERVER_ERROR);
}

Copilot is powered by AI and may make mistakes. Always verify output.
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
// 응답 바이트를 파일로 저장
byte[] fileBytes = response.getBody();
tempFile = File.createTempFile("contract_", ".pdf");

Check warning

Code scanning / CodeQL

Local information disclosure in a temporary directory Medium

Local information disclosure vulnerability due to use of file readable by other local users.

Copilot Autofix

AI 6 months ago

To fix the problem, replace the use of File.createTempFile with the secure alternative from the JDK: Files.createTempFile. This method creates a file with permissions set to be readable and writable only by the owner (-rw-------) on Unix-like systems. The returned object is a Path, but you can convert it to a File if needed.
Specifically, in src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java, replace the line:

tempFile = File.createTempFile("contract_", ".pdf");

with:

tempFile = Files.createTempFile("contract_", ".pdf").toFile();

No additional imports are needed since java.nio.file.Files is already imported. This change ensures the temporary file is created with secure permissions, preventing local information disclosure.


Suggested changeset 1
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
--- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
+++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
@@ -1139,7 +1139,7 @@
                             if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
                                 // 응답 바이트를 파일로 저장
                                 byte[] fileBytes = response.getBody();
-                                tempFile = File.createTempFile("contract_", ".pdf");
+                                tempFile = Files.createTempFile("contract_", ".pdf").toFile();
                                 Files.write(tempFile.toPath(), fileBytes);
 
                             } else {
EOF
@@ -1139,7 +1139,7 @@
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
// 응답 바이트를 파일로 저장
byte[] fileBytes = response.getBody();
tempFile = File.createTempFile("contract_", ".pdf");
tempFile = Files.createTempFile("contract_", ".pdf").toFile();
Files.write(tempFile.toPath(), fileBytes);

} else {
Copilot is powered by AI and may make mistakes. Always verify output.
}
// Remove newlines and carriage returns
responseBodyStr = responseBodyStr.replaceAll("[\\r\\n]", " ");
log.error(responseBodyStr);

Check failure

Code scanning / CodeQL

Log Injection High

This log entry depends on a
user-provided value
.

Copilot Autofix

AI 6 months ago

To fully mitigate log injection, we should sanitize the response body string before logging by removing all control characters (not just newlines and carriage returns). This can be done using a regular expression that removes all characters in the Unicode control character range (\p{Cntrl}), which includes newlines, carriage returns, tabs, and other non-printable characters. Additionally, it is good practice to clearly mark user input in log entries, e.g., by prefixing with a label. The fix should be applied in the block where responseBodyStr is logged (lines 1146–1157). No new methods are needed, but we should update the sanitization logic.


Suggested changeset 1
src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
--- a/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
+++ b/src/main/java/org/scoula/domain/contract/service/ContractServiceImpl.java
@@ -1151,9 +1151,9 @@
                                 } catch (Exception ex) {
                                     responseBodyStr = String.valueOf(response.getBody());
                                 }
-                                // Remove newlines and carriage returns
-                                responseBodyStr = responseBodyStr.replaceAll("[\\r\\n]", " ");
-                                log.error(responseBodyStr);
+                                // Remove all control characters (including newlines, carriage returns, tabs, etc.)
+                                responseBodyStr = responseBodyStr.replaceAll("\\p{Cntrl}", " ");
+                                log.error("AI server error response body: [{}]", responseBodyStr);
                                 throw new BusinessException(ContractException.CONTRACT_AI_SERVER_ERROR);
                             }
 
EOF
@@ -1151,9 +1151,9 @@
} catch (Exception ex) {
responseBodyStr = String.valueOf(response.getBody());
}
// Remove newlines and carriage returns
responseBodyStr = responseBodyStr.replaceAll("[\\r\\n]", " ");
log.error(responseBodyStr);
// Remove all control characters (including newlines, carriage returns, tabs, etc.)
responseBodyStr = responseBodyStr.replaceAll("\\p{Cntrl}", " ");
log.error("AI server error response body: [{}]", responseBodyStr);
throw new BusinessException(ContractException.CONTRACT_AI_SERVER_ERROR);
}

Copilot is powered by AI and may make mistakes. Always verify output.
public static File toTempFile(MultipartFile multipart, String prefix, String suffix)
throws IOException {
Objects.requireNonNull(multipart, "multipart must not be null");
File temp = File.createTempFile(prefix, suffix);

Check warning

Code scanning / CodeQL

Local information disclosure in a temporary directory Medium

Local information disclosure vulnerability due to use of file readable by other local users.

Copilot Autofix

AI 6 months ago

To fix the problem, replace the use of File.createTempFile(prefix, suffix) with Files.createTempFile(prefix, suffix), which returns a Path with secure permissions. Then, convert the resulting Path to a File using .toFile() to preserve the method's return type. This change should be made in both toTempFile (line 30) and inputStreamToTempFile (line 49). No additional imports are needed, as java.nio.file.Files is already imported. This change will ensure that temporary files are created with restrictive permissions, mitigating the local information disclosure risk, while preserving existing functionality.

Suggested changeset 1
src/main/java/org/scoula/global/common/util/MultipartFileUtils.java

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/main/java/org/scoula/global/common/util/MultipartFileUtils.java b/src/main/java/org/scoula/global/common/util/MultipartFileUtils.java
--- a/src/main/java/org/scoula/global/common/util/MultipartFileUtils.java
+++ b/src/main/java/org/scoula/global/common/util/MultipartFileUtils.java
@@ -27,7 +27,7 @@
       public static File toTempFile(MultipartFile multipart, String prefix, String suffix)
               throws IOException {
           Objects.requireNonNull(multipart, "multipart must not be null");
-          File temp = File.createTempFile(prefix, suffix);
+          File temp = Files.createTempFile(prefix, suffix).toFile();
           try (InputStream in = multipart.getInputStream()) {
               Files.copy(in, temp.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
           }
@@ -46,7 +46,7 @@
           if (prefix == null || prefix.isBlank()) prefix = "contract_";
           if (suffix == null || suffix.isBlank()) suffix = ".pdf";
 
-          File temp = File.createTempFile(prefix, suffix);
+          File temp = Files.createTempFile(prefix, suffix).toFile();
           try (InputStream src = in) {
               Files.copy(src, temp.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
           }
EOF
@@ -27,7 +27,7 @@
public static File toTempFile(MultipartFile multipart, String prefix, String suffix)
throws IOException {
Objects.requireNonNull(multipart, "multipart must not be null");
File temp = File.createTempFile(prefix, suffix);
File temp = Files.createTempFile(prefix, suffix).toFile();
try (InputStream in = multipart.getInputStream()) {
Files.copy(in, temp.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
@@ -46,7 +46,7 @@
if (prefix == null || prefix.isBlank()) prefix = "contract_";
if (suffix == null || suffix.isBlank()) suffix = ".pdf";

File temp = File.createTempFile(prefix, suffix);
File temp = Files.createTempFile(prefix, suffix).toFile();
try (InputStream src = in) {
Files.copy(src, temp.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
Copilot is powered by AI and may make mistakes. Always verify output.
if (prefix == null || prefix.isBlank()) prefix = "contract_";
if (suffix == null || suffix.isBlank()) suffix = ".pdf";

File temp = File.createTempFile(prefix, suffix);

Check warning

Code scanning / CodeQL

Local information disclosure in a temporary directory Medium

Local information disclosure vulnerability due to use of file readable by other local users.

Copilot Autofix

AI 6 months ago

To fix the problem, we should use the java.nio.file.Files.createTempFile method, which creates temporary files with secure permissions (-rw-------) by default on Unix-like systems. This method returns a Path, which can be converted to a File if needed. We should replace all instances of File.createTempFile(prefix, suffix) with Files.createTempFile(prefix, suffix).toFile(). This change should be made in both the toTempFile and inputStreamToTempFile methods. No additional imports are needed, as java.nio.file.Files is already imported.

Suggested changeset 1
src/main/java/org/scoula/global/common/util/MultipartFileUtils.java

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/main/java/org/scoula/global/common/util/MultipartFileUtils.java b/src/main/java/org/scoula/global/common/util/MultipartFileUtils.java
--- a/src/main/java/org/scoula/global/common/util/MultipartFileUtils.java
+++ b/src/main/java/org/scoula/global/common/util/MultipartFileUtils.java
@@ -27,7 +27,7 @@
       public static File toTempFile(MultipartFile multipart, String prefix, String suffix)
               throws IOException {
           Objects.requireNonNull(multipart, "multipart must not be null");
-          File temp = File.createTempFile(prefix, suffix);
+          File temp = Files.createTempFile(prefix, suffix).toFile();
           try (InputStream in = multipart.getInputStream()) {
               Files.copy(in, temp.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
           }
@@ -46,7 +46,7 @@
           if (prefix == null || prefix.isBlank()) prefix = "contract_";
           if (suffix == null || suffix.isBlank()) suffix = ".pdf";
 
-          File temp = File.createTempFile(prefix, suffix);
+          File temp = Files.createTempFile(prefix, suffix).toFile();
           try (InputStream src = in) {
               Files.copy(src, temp.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
           }
EOF
@@ -27,7 +27,7 @@
public static File toTempFile(MultipartFile multipart, String prefix, String suffix)
throws IOException {
Objects.requireNonNull(multipart, "multipart must not be null");
File temp = File.createTempFile(prefix, suffix);
File temp = Files.createTempFile(prefix, suffix).toFile();
try (InputStream in = multipart.getInputStream()) {
Files.copy(in, temp.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
@@ -46,7 +46,7 @@
if (prefix == null || prefix.isBlank()) prefix = "contract_";
if (suffix == null || suffix.isBlank()) suffix = ".pdf";

File temp = File.createTempFile(prefix, suffix);
File temp = Files.createTempFile(prefix, suffix).toFile();
try (InputStream src = in) {
Files.copy(src, temp.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
Copilot is powered by AI and may make mistakes. Always verify output.

private static String probeContentType(File file) {
try {
String ct = Files.probeContentType(file.toPath());

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
This path depends on a user-provided value.

Copilot Autofix

AI 6 months ago

To fix the problem, we need to validate the extension extracted from the user-controlled filename before using it in File.createTempFile. Specifically, we should ensure that the extension does not contain any path separators (/, \), parent directory references (..), or other potentially dangerous characters. The extension should match a safe pattern, such as starting with a dot and containing only alphanumeric characters (and possibly a limited set of other safe characters like underscores or hyphens). If the extension fails validation, we should default to a safe value (e.g., .pdf). The validation should be added in EncryptionServiceImpl.java where the extension is extracted and used.


Suggested changeset 1
src/main/java/org/scoula/global/common/service/EncryptionServiceImpl.java
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/main/java/org/scoula/global/common/service/EncryptionServiceImpl.java b/src/main/java/org/scoula/global/common/service/EncryptionServiceImpl.java
--- a/src/main/java/org/scoula/global/common/service/EncryptionServiceImpl.java
+++ b/src/main/java/org/scoula/global/common/service/EncryptionServiceImpl.java
@@ -331,8 +331,8 @@
               }
           }
 
-          // 확장자가 없으면 기본값 설정
-          if (extension.isEmpty()) {
+          // 확장자가 없거나, 확장자가 안전하지 않으면 기본값 설정
+          if (extension.isEmpty() || !isSafeExtension(extension)) {
               extension = ".pdf"; // PDF의 기본 확장자
           }
 
@@ -353,6 +353,15 @@
           return tempFile;
       }
 
+      /**
+       * 확장자가 안전한지 검사 (".pdf", ".png" 등, 점으로 시작하고 알파벳/숫자/언더스코어/하이픈만 허용)
+       */
+      private static boolean isSafeExtension(String ext) {
+          if (ext == null) return false;
+          // Must start with a dot, and only contain [a-zA-Z0-9_-] after the dot
+          return ext.matches("^\\.[a-zA-Z0-9_-]+$");
+      }
+
       /** 2-of-3 암호화된 PDF 복호화 커스텀 파일 포맷에서 shares를 추출하여 복호화 */
       private byte[] decrypt2Of3Pdf(byte[] fileData, String password) throws Exception {
           log.info("Starting 2-of-3 PDF decryption");
EOF
@@ -331,8 +331,8 @@
}
}

// 확장자가 없으면 기본값 설정
if (extension.isEmpty()) {
// 확장자가 없거나, 확장자가 안전하지 않으면 기본값 설정
if (extension.isEmpty() || !isSafeExtension(extension)) {
extension = ".pdf"; // PDF의 기본 확장자
}

@@ -353,6 +353,15 @@
return tempFile;
}

/**
* 확장자가 안전한지 검사 (".pdf", ".png" , 점으로 시작하고 알파벳/숫자/언더스코어/하이픈만 허용)
*/
private static boolean isSafeExtension(String ext) {
if (ext == null) return false;
// Must start with a dot, and only contain [a-zA-Z0-9_-] after the dot
return ext.matches("^\\.[a-zA-Z0-9_-]+$");
}

/** 2-of-3 암호화된 PDF 복호화 커스텀 파일 포맷에서 shares를 추출하여 복호화 */
private byte[] decrypt2Of3Pdf(byte[] fileData, String password) throws Exception {
log.info("Starting 2-of-3 PDF decryption");
Copilot is powered by AI and may make mistakes. Always verify output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feature 새로운 기능 요청

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] 05 내보내기 [Feat] 이슈 제목

1 participant