Skip to content

Commit d6a7ba2

Browse files
committed
fix: @DecodeHash의 raw numeric ID 통과 취약점 수정 및 검증 인터셉터 도입 (#55)
1 parent 26e9e9f commit d6a7ba2

12 files changed

Lines changed: 170 additions & 45 deletions

File tree

src/main/java/com/sofa/linkiving/domain/link/controller/LinkApi.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ BaseResponse<SummaryRes> updateSummary(
149149
);
150150

151151
@Operation(summary = "요약 상태 조회", description = "링크 요약 상태를 조회합니다.")
152-
BaseResponse<SummaryStatusRes<?>> getSummaryStatus(
152+
BaseResponse<SummaryStatusRes> getSummaryStatus(
153153
Long id,
154154
Member member
155155
);

src/main/java/com/sofa/linkiving/domain/link/controller/LinkController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,11 +189,11 @@ public BaseResponse<Void> retrySummary(
189189

190190
@Override
191191
@GetMapping("/{id}/summary-status")
192-
public BaseResponse<SummaryStatusRes<?>> getSummaryStatus(
192+
public BaseResponse<SummaryStatusRes> getSummaryStatus(
193193
@PathVariable @DecodeHash Long id,
194194
@AuthMember Member member
195195
) {
196-
SummaryStatusRes<?> response = linkFacade.getSummaryStatus(id, member);
196+
SummaryStatusRes response = linkFacade.getSummaryStatus(id, member);
197197
return BaseResponse.success(response, "요약 상태 조회 완료");
198198
}
199199
}

src/main/java/com/sofa/linkiving/domain/link/dto/response/LinkCardRes.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
import com.sofa.linkiving.domain.link.dto.internal.LinkDto;
55
import com.sofa.linkiving.domain.link.entity.Link;
66
import com.sofa.linkiving.domain.link.entity.Summary;
7-
import com.sofa.linkiving.global.config.jackson.HashidsSerializer;
87
import com.sofa.linkiving.domain.link.enums.SummaryStatus;
8+
import com.sofa.linkiving.global.config.jackson.HashidsSerializer;
99

1010
import io.swagger.v3.oas.annotations.media.Schema;
1111

src/main/java/com/sofa/linkiving/domain/link/dto/response/LinkDetailRes.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
import com.sofa.linkiving.domain.link.dto.internal.LinkDto;
55
import com.sofa.linkiving.domain.link.entity.Link;
66
import com.sofa.linkiving.domain.link.entity.Summary;
7-
import com.sofa.linkiving.global.config.jackson.HashidsSerializer;
87
import com.sofa.linkiving.domain.link.enums.SummaryStatus;
8+
import com.sofa.linkiving.global.config.jackson.HashidsSerializer;
99

1010
import io.swagger.v3.oas.annotations.media.Schema;
1111

src/main/java/com/sofa/linkiving/domain/link/dto/response/LinkRes.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
44
import com.sofa.linkiving.domain.link.entity.Link;
5-
import com.sofa.linkiving.global.config.jackson.HashidsSerializer;
65
import com.sofa.linkiving.domain.link.enums.SummaryStatus;
6+
import com.sofa.linkiving.global.config.jackson.HashidsSerializer;
77

88
import io.swagger.v3.oas.annotations.media.Schema;
99

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package com.sofa.linkiving.global.config.web;
2+
3+
import java.lang.reflect.Method;
4+
import java.lang.reflect.Parameter;
5+
import java.util.HashMap;
6+
import java.util.Map;
7+
8+
import org.springframework.stereotype.Component;
9+
import org.springframework.util.ClassUtils;
10+
import org.springframework.util.StringUtils;
11+
import org.springframework.web.bind.annotation.PathVariable;
12+
import org.springframework.web.bind.annotation.RequestParam;
13+
import org.springframework.web.method.HandlerMethod;
14+
import org.springframework.web.servlet.HandlerInterceptor;
15+
import org.springframework.web.servlet.HandlerMapping;
16+
17+
import com.sofa.linkiving.global.config.annotation.DecodeHash;
18+
import com.sofa.linkiving.global.util.HashidsUtils;
19+
20+
import jakarta.annotation.Nonnull;
21+
import jakarta.servlet.http.HttpServletRequest;
22+
import jakarta.servlet.http.HttpServletResponse;
23+
import lombok.RequiredArgsConstructor;
24+
import lombok.extern.slf4j.Slf4j;
25+
26+
@Slf4j
27+
@Component
28+
@RequiredArgsConstructor
29+
public class DecodeHashInterceptor implements HandlerInterceptor {
30+
31+
private final HashidsUtils hashidsUtils;
32+
33+
@Override
34+
public boolean preHandle(@Nonnull HttpServletRequest request, @Nonnull HttpServletResponse response,
35+
@Nonnull Object handler) {
36+
if (!(handler instanceof HandlerMethod handlerMethod)) {
37+
return true;
38+
}
39+
40+
Object attribute = request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
41+
Map<String, String> pathVars = new HashMap<>();
42+
43+
if (attribute instanceof Map<?, ?> map) {
44+
for (Map.Entry<?, ?> entry : map.entrySet()) {
45+
if (entry.getKey() instanceof String key && entry.getValue() instanceof String value) {
46+
pathVars.put(key, value);
47+
}
48+
}
49+
}
50+
51+
Method specificMethod = ClassUtils.getMostSpecificMethod(handlerMethod.getMethod(),
52+
handlerMethod.getBeanType());
53+
54+
for (Parameter param : specificMethod.getParameters()) {
55+
56+
if (param.isAnnotationPresent(DecodeHash.class)) {
57+
String rawValue = null;
58+
59+
if (param.isAnnotationPresent(PathVariable.class)) {
60+
PathVariable pv = param.getAnnotation(PathVariable.class);
61+
String name = StringUtils.hasText(pv.value()) ? pv.value() : pv.name();
62+
if (!StringUtils.hasText(name)) {
63+
name = param.getName();
64+
}
65+
66+
if (name != null) {
67+
rawValue = pathVars.get(name);
68+
}
69+
} else if (param.isAnnotationPresent(RequestParam.class)) {
70+
RequestParam rp = param.getAnnotation(RequestParam.class);
71+
String name = StringUtils.hasText(rp.value()) ? rp.value() : rp.name();
72+
if (!StringUtils.hasText(name)) {
73+
name = param.getName();
74+
}
75+
76+
if (name != null) {
77+
rawValue = request.getParameter(name);
78+
}
79+
}
80+
81+
if (StringUtils.hasText(rawValue)) {
82+
hashidsUtils.decode(rawValue);
83+
}
84+
}
85+
}
86+
return true;
87+
}
88+
}
Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package com.sofa.linkiving.global.config.web;
22

3-
import java.text.ParseException;
4-
import java.util.Locale;
53
import java.util.Set;
64

75
import org.springframework.format.AnnotationFormatterFactory;
@@ -12,6 +10,7 @@
1210
import com.sofa.linkiving.global.config.annotation.DecodeHash;
1311
import com.sofa.linkiving.global.util.HashidsUtils;
1412

13+
import jakarta.annotation.Nonnull;
1514
import lombok.RequiredArgsConstructor;
1615

1716
@Component
@@ -20,28 +19,21 @@ public class HashidsFormatterFactory implements AnnotationFormatterFactory<Decod
2019

2120
private final HashidsUtils hashidsUtils;
2221

22+
@Nonnull
2323
@Override
2424
public Set<Class<?>> getFieldTypes() {
2525
return Set.of(Long.class);
2626
}
2727

28+
@Nonnull
2829
@Override
29-
public Parser<?> getParser(DecodeHash annotation, Class<?> fieldType) {
30-
return new Parser<Long>() {
31-
@Override
32-
public Long parse(String text, Locale locale) throws ParseException {
33-
return hashidsUtils.decode(text);
34-
}
35-
};
30+
public Parser<?> getParser(@Nonnull DecodeHash annotation, @Nonnull Class<?> fieldType) {
31+
return (Parser<Long>)(text, locale) -> hashidsUtils.decode(text);
3632
}
3733

34+
@Nonnull
3835
@Override
39-
public Printer<?> getPrinter(DecodeHash annotation, Class<?> fieldType) {
40-
return new Printer<Long>() {
41-
@Override
42-
public String print(Long object, Locale locale) {
43-
return hashidsUtils.encode(object);
44-
}
45-
};
36+
public Printer<?> getPrinter(@Nonnull DecodeHash annotation, @Nonnull Class<?> fieldType) {
37+
return (Printer<Long>)(object, locale) -> hashidsUtils.encode(object);
4638
}
4739
}

src/main/java/com/sofa/linkiving/global/config/web/WebMvcConfig.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
99
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
1010
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
11+
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
1112
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
1213

1314
import com.sofa.linkiving.security.resolver.AuthMemberArgumentResolver;
@@ -20,6 +21,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
2021

2122
private final AuthMemberArgumentResolver authMemberArgumentResolver;
2223
private final HashidsFormatterFactory hashidsFormatterFactory;
24+
private final DecodeHashInterceptor decodeHashInterceptor;
2325

2426
@Override
2527
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
@@ -45,4 +47,9 @@ public AsyncTaskExecutor mvcTaskExecutor() {
4547
public void addFormatters(FormatterRegistry registry) {
4648
registry.addFormatterForFieldAnnotation(hashidsFormatterFactory);
4749
}
50+
51+
@Override
52+
public void addInterceptors(InterceptorRegistry registry) {
53+
registry.addInterceptor(decodeHashInterceptor);
54+
}
4855
}

src/main/java/com/sofa/linkiving/global/error/code/CommonErrorCode.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ public enum CommonErrorCode implements ErrorCode {
1919
METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "C-007", "허용되지 않은 HTTP 메소드입니다."),
2020
FORBIDDEN(HttpStatus.FORBIDDEN, "C-008", "접근이 허용되지 않았습니다."),
2121
NULL_POINTER(HttpStatus.INTERNAL_SERVER_ERROR, "C-009", "NullPointerException이 발생했습니다."),
22-
HTTP_MEDIA_NOT_SUPPORT(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "C-010", "지원하지 않는 미디어입니다.");
22+
HTTP_MEDIA_NOT_SUPPORT(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "C-010", "지원하지 않는 미디어입니다."),
23+
INVALID_IDENTIFIER(HttpStatus.BAD_REQUEST, "C-011", "유효하지 않은 식별자입니다.");
2324

2425
private final HttpStatus status;
2526
private final String code;

src/main/java/com/sofa/linkiving/global/util/HashidsUtils.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@
44
import org.springframework.beans.factory.annotation.Value;
55
import org.springframework.stereotype.Component;
66

7+
import com.sofa.linkiving.global.error.code.CommonErrorCode;
8+
import com.sofa.linkiving.global.error.exception.BusinessException;
9+
10+
import lombok.extern.slf4j.Slf4j;
11+
712
@Component
13+
@Slf4j
814
public class HashidsUtils {
915
private final Hashids hashids;
1016

@@ -30,7 +36,7 @@ public Long decode(String hash) {
3036
long[] decoded = hashids.decode(hash);
3137

3238
if (decoded.length == 0) {
33-
throw new IllegalArgumentException("유효하지 않은 식별자입니다.");
39+
throw new BusinessException(CommonErrorCode.INVALID_IDENTIFIER);
3440
}
3541
return decoded[0];
3642
}

0 commit comments

Comments
 (0)