Skip to content

ProductLink 파싱 검증을 도메인 예외로 분리#118

Merged
m-a-king merged 2 commits into
devfrom
fix/114-productlink-url-leak
May 15, 2026
Merged

ProductLink 파싱 검증을 도메인 예외로 분리#118
m-a-king merged 2 commits into
devfrom
fix/114-productlink-url-leak

Conversation

@m-a-king

@m-a-king m-a-king commented May 13, 2026

Copy link
Copy Markdown
Collaborator

Situation

  • ProductLink.parse 가 검증 실패 시 IllegalArgumentException("…: $trimmed", e) 로 원본 URL 을 메시지에 담고 있었다.
  • GlobalExceptionHandler.handleIllegalArgumente.message 를 응답 detail 과 로그 양쪽에 그대로 박는 구조 → 쿼리스트링/fragment 에 섞인 토큰·세션이 외부로 새는 경로.
  • 같은 파일 line 10 의 safeLogString() 주석이 이미 "쿼리스트링·fragment 에 토큰/세션이 섞일 수 있어 host+path 만 노출한다" 라고 명시 — 파일이 위험을 인식하면서도 parse() 만 inconsistency.

Task

  • 메시지 노출 정책을 도메인 측에서 명시적으로 통제.
  • IllegalArgumentException 일반 핸들러 정책에 묶이지 않게 분리.
  • 회귀를 잡을 수 있도록 단위 · 통합 양쪽 contract 단언 추가.

Action

  • ProductLinkException 추가 (BaseException + HttpMappable, HTTP 400 + INVALID_INPUT).
  • 코드베이스 컨벤션(WishException, ProductExtractionException) 그대로 — companion object 정적 팩토리 메서드 패턴:
    • blank() — 빈 입력
    • invalidFormat(cause: Throwable) — URI 파싱 실패. 원본을 message 에 박지 않고 cause 로만 연결해 stack trace 에 남김.
    • unsupportedScheme() — https 외 스킴
  • ProductLink.parserequire · throw 를 모두 새 예외로 전환. IllegalArgumentException 경로 제거.
  • 단위 테스트 (ProductLinkTest) — ProductLinkException 으로 단언 전환, 원본 raw 가 message 에 포함되지 않음 명시 단언 추가.
  • 통합 테스트 (WishlistControllerIntegrationTest) — 잘못된 형식 URL 요청 시 응답 status · code · detail contract 단언으로 회귀 가드.

Result

  • 응답 detail / 로그 양쪽으로 원본 URL 이 새지 않음.
  • IllegalArgumentException 일반 핸들러 정책에 묶이지 않고, ProductLink 메시지 정책이 도메인 안에서 통제됨.
  • 후속 고려: GlobalExceptionHandler.handleIllegalArgument 가 4xx 응답 detail 에 e.message 를 무방비로 박는 패턴 자체도 잠재 위험. 다른 4xx 응답 contract 와 얽혀 영향도 평가가 별도로 필요하므로 본 PR 스코프 밖. 필요하면 별도 이슈로.

연관 이슈

Summary by CodeRabbit

  • Bug Fixes
    • 제품 링크 URL 검증 강화: 빈 입력, 잘못된 형식, 비(非)HTTPS 스킴에 대해 명확한 오류 응답을 반환하고 원본 URL 내용이 에러 메시지에 노출되지 않도록 수정
  • Tests
    • URL 검증 및 에러 응답(400) 동작과 민감 정보 비노출을 검증하는 단위/통합 테스트 추가

Review Change Stack

`GlobalExceptionHandler.handleIllegalArgument` 가 `e.message` 를 응답
detail · 로그 양쪽에 그대로 박는 구조라, `ProductLink.parse` 가 원본 URL 을
메시지에 담으면 쿼리스트링/fragment 에 섞일 수 있는 토큰·세션이 외부로 새는
경로가 됐다.

`IllegalArgumentException` 으로 던지면 일반 핸들러 정책에 묶여 detail
노출이 도메인에서 통제되지 않는다. 코드베이스 컨벤션 (`WishException`,
`ProductExtractionException`) 처럼 도메인 전용 예외 + companion object
정적 팩토리 메서드 패턴으로 분리한다.

- `ProductLinkException` 추가 (BaseException + HttpMappable,
  HTTP 400 + INVALID_INPUT). 정적 팩토리: `blank()`, `invalidFormat(cause)`,
  `unsupportedScheme()`. `invalidFormat` 은 원본을 message 에 박지 않고
  cause 로만 연결해 stack trace 에 남긴다.
- `ProductLink.parse` 의 require · throw 를 모두 새 예외로 전환.
- 단위 테스트 (`ProductLinkTest`): 원본 raw 가 message 에 포함되지 않음
  명시 단언.
- 통합 테스트 (`WishlistControllerIntegrationTest`): 응답 contract
  (status·code·detail) 까지 회귀 검증.
@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: c5e586b4-a271-4d8d-9690-c3103b3695db

📥 Commits

Reviewing files that changed from the base of the PR and between f76fe58 and d4002d8.

📒 Files selected for processing (1)
  • src/test/kotlin/com/depromeet/team3/product/domain/ProductLinkTest.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/test/kotlin/com/depromeet/team3/product/domain/ProductLinkTest.kt

Walkthrough

URL 파싱 검증을 ProductLinkException으로 통합하고 고정된 예외 메시지를 사용하여 민감한 정보 노출을 방지합니다. 단위 및 통합 테스트로 토큰 유출 방지를 검증합니다.

Changes

ProductLink 파싱 보안 개선

Layer / File(s) Summary
ProductLinkException 예외 클래스 정의
src/main/kotlin/com/depromeet/team3/product/domain/ProductLinkException.kt
BaseException을 확장하고 HttpMappable을 구현하는 ProductLinkException을 도입합니다. blank(), invalidFormat(cause), unsupportedScheme() 팩토리 메서드는 ErrorCategory.INVALID_INPUT과 HttpStatus.BAD_REQUEST를 사용하며, 원본 URL을 포함하지 않은 고정 메시지를 제공합니다.
ProductLink.parse() 메서드 리팩토링
src/main/kotlin/com/depromeet/team3/product/domain/ProductLink.kt
parse() 메서드의 검증 로직을 개선하여 입력을 trim하고, blank 입력, URI 파싱 실패(IllegalArgumentException 포착), 지원하지 않는 스킴을 ProductLinkException으로 명시적으로 처리합니다. 기존 require 기반의 검증을 대체합니다.
단위 테스트 업데이트 및 보안 검증
src/test/kotlin/com/depromeet/team3/product/domain/ProductLinkTest.kt
모든 검증 실패 케이스가 ProductLinkException을 발생시키는지 확인하고, 데이터 URI 파싱 실패 시 예외 메시지가 "유효한 URL 형식이 아닙니다."로 고정되어 있는지 및 입력 URL에 포함된 토큰이 예외 메시지에 노출되지 않는지 명시적으로 테스트합니다.
통합 테스트: API 응답 보안 검증
src/test/kotlin/com/depromeet/team3/wishlist/controller/WishlistControllerIntegrationTest.kt
잘못된 형식의 URL(토큰 포함)이 API 요청으로 들어올 때 400 BAD_REQUEST를 반환하고, detail 필드에 고정 메시지만 포함되며 원본 URL이 노출되지 않는지 엔드-투-엔드로 검증합니다.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Assessment against linked issues

Objective Addressed Explanation
원본 URL이 응답 detail에 노출되지 않도록 해결 [#114]
parse() 예외 메시지에서 원본 URL 제거하고 고정 메시지 사용 [#114]
토큰/세션 포함 URL이 로그에 노출되지 않도록 예외 설계 [#114] GlobalExceptionHandler의 로그/응답 핸들링 전반은 본 PR 범위 밖이며, cause나 handler의 로그 사용으로 유출 가능성 여부는 핸들러 코드 검토가 필요합니다.

리뷰 포인트

👍 잘 구현된 부분

짧고 굵게: 예외 설계로 실수 노출 가능성을 구조적으로 막은 점, 테스트로 회귀 방지를 명시한 점 모두 굿.

고려할 사항 (구체적 조치 제안)

  1. cause 노출 위험 확인

    • GlobalExceptionHandler가 예외의 cause.message나 toString()을 로그/응답에 그대로 쓰지 않는지 확인하세요. (참고: Spring의 DefaultHandler는 종종 exception.getMessage()를 그대로 사용합니다.)
    • 권장: handleIllegalArgument 등에서 e.getMessage() 대신 HttpMappable 인터페이스를 우선 사용하거나, ProductLinkException이면 고정 메시지만 노출하도록 분기 처리하세요.
  2. 로그 수준과 내용 통일화

    • 내부 로깅에는 cause를 남기되 (debug/trace), warn/info 레벨 로그와 응답에는 고정 메시지만 사용하세요.
    • 참고문서: OWASP Logging Cheat Sheet.
  3. 추가 테스트 제안

    • "대소문자 섞인 scheme" 케이스("hTTps://...")와 스킴 누락 케이스에 대한 추가 단위 테스트를 권장합니다(현재 코드 경로가 테스트로 충분히 커버되는지 확인).

짧고 위트있게: 안전한 예외처리, 칭찬드립니다 — 토큰을 숨기는 데 성공했어요.

🚥 Pre-merge checks | ✅ 1 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/114-productlink-url-leak

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

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

@github-actions

Copy link
Copy Markdown

No description provided.

@theo-s-park

Copy link
Copy Markdown

:)

@m-a-king m-a-king self-assigned this May 13, 2026

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
src/test/kotlin/com/depromeet/team3/product/domain/ProductLinkTest.kt (1)

41-48: ⚡ Quick win

테스트 단언 일관성 개선

Line 47의 startsWith 검증은 Line 59의 새로운 테스트와 일관성이 없습니다. ProductLinkException.invalidFormat의 메시지는 고정되어 있으므로 ("유효한 URL 형식이 아닙니다.") 정확한 일치를 검증하는 것이 더 명확하고 회귀 방지에 유리합니다.

♻️ 제안하는 수정
-        assertTrue(ex.message?.startsWith("유효한 URL 형식이 아닙니다") == true)
+        assertEquals("유효한 URL 형식이 아닙니다.", ex.message)
🤖 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/test/kotlin/com/depromeet/team3/product/domain/ProductLinkTest.kt` around
lines 41 - 48, The assertion in the test `URI 파싱 자체가 실패하는 raw 는 형식 오류로 거부한다`
should check for exact message equality rather than startsWith for
`ProductLinkException.invalidFormat`; update the assertion that inspects
ex.message (raised from ProductLink.parse) to assert equality with the fixed
message "유효한 URL 형식이 아닙니다." so it matches the canonical
ProductLinkException.invalidFormat text and aligns with the other test.
🤖 Prompt for all review comments with 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.

Nitpick comments:
In `@src/test/kotlin/com/depromeet/team3/product/domain/ProductLinkTest.kt`:
- Around line 41-48: The assertion in the test `URI 파싱 자체가 실패하는 raw 는 형식 오류로
거부한다` should check for exact message equality rather than startsWith for
`ProductLinkException.invalidFormat`; update the assertion that inspects
ex.message (raised from ProductLink.parse) to assert equality with the fixed
message "유효한 URL 형식이 아닙니다." so it matches the canonical
ProductLinkException.invalidFormat text and aligns with the other test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: ab4fe9b9-23ea-4b6e-b21a-2d93cf68a4a0

📥 Commits

Reviewing files that changed from the base of the PR and between 04d4936 and f76fe58.

📒 Files selected for processing (4)
  • src/main/kotlin/com/depromeet/team3/product/domain/ProductLink.kt
  • src/main/kotlin/com/depromeet/team3/product/domain/ProductLinkException.kt
  • src/test/kotlin/com/depromeet/team3/product/domain/ProductLinkTest.kt
  • src/test/kotlin/com/depromeet/team3/wishlist/controller/WishlistControllerIntegrationTest.kt

@github-actions github-actions Bot requested review from 1o18z and sevineleven May 13, 2026 08:26
Comment on lines +59 to +60
assertEquals("유효한 URL 형식이 아닙니다.", ex.message)
assertFalse(ex.message!!.contains("SHOULD_NOT_LEAK"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

요거 중복 아닌가 ???

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

맞습니다 — 정확한 지적입니다. 바로 위 줄의 assertEquals("유효한 URL 형식이 아닙니다.", ex.message) 가 통과하면 assertFalse(contains) 는 항상 true 라 redundant 했어요. "원본이 안 박힌다" 의도를 명시적으로 박고 싶어서 두 줄로 두긴 했는데, equality 단언이 더 강한 보장이라 한 줄로 충분합니다.

d4002d8 에서 assertFalse 줄 제거했습니다. 동일 커밋에서 위쪽 URI 파싱 자체가 실패하는 raw… 테스트의 startsWithassertEquals 로 일관화했고요 (CodeRabbit nitpick 함께 반영).

@sevineleven

Copy link
Copy Markdown
Collaborator

좋아 💯

- '유효한 URL 형식이 아닙니다' 케이스를 startsWith → assertEquals 로 정확
  일치 단언. 메시지가 고정 문구라 equality 가 회귀 검출에 강하고 같은 파일
  안 다른 단언들과 일관됨.
- '예외 메시지에 원본 raw 는 포함되지 않는다' 테스트에서 assertEquals
  뒤에 따라붙던 assertFalse(contains) 제거. equality 단언이 통과하면
  contains 단언은 항상 true 라 redundant.
@m-a-king m-a-king merged commit aaeeed1 into dev May 15, 2026
10 checks passed
@m-a-king m-a-king deleted the fix/114-productlink-url-leak branch May 15, 2026 07:31
sevineleven added a commit that referenced this pull request May 16, 2026
- dev #118 이 추가한 통합 테스트가 요청 본문에 guestId 필드를 쓰는데
  본 PR 의 Guest 도메인 제거 변경과 의미적으로 충돌해 dev 머지 후 CI 가 실패
- 같은 파일의 다른 케이스와 동일하게 userId 로 통일
@m-a-king m-a-king added the fix 외부 가시적 결함 수정 label May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix 외부 가시적 결함 수정

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ProductLink 파싱 실패 시 원본 URL 이 응답·로그에 노출됨

3 participants