링크 저장 URL 정규화 및 검증 적용 - #300
Conversation
📊 코드 커버리지 리포트
|
| public String normalize(String rawUrl) { | ||
| if (rawUrl == null || rawUrl.isBlank()) { | ||
| throw new BusinessException(LinkErrorCode.INVALID_URL); | ||
| } | ||
|
|
||
| String normalizedUrl = removeDuplicatedHttpScheme(rawUrl.strip()); | ||
| if (!SCHEME_PATTERN.matcher(normalizedUrl).find()) { | ||
| normalizedUrl = "https://" + normalizedUrl; | ||
| } | ||
|
|
||
| validateHost(normalizedUrl); |
There was a problem hiding this comment.
normalize() 가 대소문자를 보존해서, 같은 링크가 케이스만 다르면 서로 다른 문자열로 저장됩니다.
중복 판정이 existsByMemberAndUrlAndIsDeleteFalse 의 문자열 동등 비교라서 아래 입력에 대해서 전부 별개 링크로 저장됩니다.
urlNormalizer.normalize("Naver.com"); // -> "https://Naver.com"
urlNormalizer.normalize("naver.com"); // -> "https://naver.com" (다른 문자열)
urlNormalizer.normalize("HTTPS://NAVER.COM"); // -> "HTTPS://NAVER.COM" (그대로 통과)validateHost 가 파싱한 URI 를 반환하게 하고, 스킴·호스트 구간만 소문자화하도록 수정 부탁드립니다.
추가로 수정 후 아래 케이스에 대한 테스트 추가를 제안합니다
"Naver.com, https://naver.com",
"HTTPS://NAVER.COM, https://naver.com",
"'https://Naver.com/Path?Q=Abc#Frag', 'https://naver.com/Path?Q=Abc#Frag'"There was a problem hiding this comment.
반영했습니다. URI 파싱 후 스킴과 호스트만 Locale.ROOT 기준으로 소문자화하고, path/query/fragment는 원문의 대소문자와 값을 그대로 보존하도록 수정했습니다. 제안해 주신 대소문자 입력 케이스도 테스트에 추가했습니다.
| @Test | ||
| @DisplayName("메타데이터 수집 전 정규화된 URL을 SSRF 검증한다") | ||
| void validateNormalizedUrlBeforeCrawling() { | ||
| UrlValidator urlValidator = mock(UrlValidator.class); | ||
| UrlNormalizer urlNormalizer = mock(UrlNormalizer.class); | ||
| OgTagCrawler ogTagCrawler = new OgTagCrawler(urlValidator, urlNormalizer); | ||
| String rawUrl = " 192.168.0.1 "; | ||
| String normalizedUrl = "https://192.168.0.1"; | ||
|
|
||
| given(urlNormalizer.normalize(rawUrl)).willReturn(normalizedUrl); | ||
| willThrow(new BusinessException(LinkErrorCode.INVALID_URL_PRIVATE_IP)) | ||
| .given(urlValidator).validateSafeUrl(normalizedUrl); | ||
|
|
||
| assertThatThrownBy(() -> ogTagCrawler.crawl(rawUrl)) | ||
| .isInstanceOf(BusinessException.class) | ||
| .hasFieldOrPropertyWithValue("errorCode", LinkErrorCode.INVALID_URL_PRIVATE_IP); | ||
|
|
||
| then(urlNormalizer).should().normalize(rawUrl); | ||
| then(urlValidator).should().validateSafeUrl(normalizedUrl); | ||
| } |
There was a problem hiding this comment.
OgTagCrawlerTest 는 "정규화된 URL 을 SSRF 검증한다"는 잘 테스트하고 있지만, "정규화된 URL 로 접속한다"는 검증하지 못하고 있습니다.
실제로 OgTagCrawler 파일을 아래와 같이 수정한 후 OgTagCrawlerTest 를 실행하면 그대로 통과하고 있습니다.
// OgTagCrawler.crawl() 에 회귀 주입: 검증은 정규화 URL, 접속은 원본 URL
Document document = Jsoup.connect(url) // normalizedUrl -> url 로 되돌려도이 상태에서 현재 테스트는 검증 스텁이 먼저 예외를 던지는 경로만 타서 Jsoup.connect(...) 라인이 실행되지 않기 때문입니다.
나중에 리팩토링 과정에서 접속 대상이 원본 url 로 돌아가는 실수가 생기면, 스킴 없는 입력(naver.com)의 meta-scrape 가 Jsoup IllegalArgumentException 으로 500 을 반환하는데 이를 잡아줄 테스트가 없습니다. 실수 방지를 위해 접속 대상까지 잠그는 테스트 보강을 부탁드립니다
There was a problem hiding this comment.
반영했습니다. MockWebServer를 이용해 스킴이 없는 원본 입력이 정규화된 서버 URL로 실제 요청되는지 검증했습니다. 요청 경로와 쿼리 파라미터까지 확인하도록 테스트를 보강했습니다.
| public OgTagCrawler(UrlValidator urlValidator, UrlNormalizer urlNormalizer) { | ||
| this.urlValidator = urlValidator; | ||
| this.urlNormalizer = urlNormalizer; | ||
| } |
There was a problem hiding this comment.
의존성 주입이 생성자 주입 방식으로 진행되고 있는데 통일성을 위해 해당 방식을 @RequiredArgsConstructor 로 수정부탁드립니다.
There was a problem hiding this comment.
반영했습니다. 직접 작성한 생성자를 제거하고 @RequiredArgsConstructor를 사용하도록 변경했습니다.
8c81acf to
e826212
Compare
e826212 to
9efce97
Compare
관련 이슈
PR 설명
검증