Pageable 악의적인 입력 방어: PageRequestDto 설계
Pageable 악의적인 입력 방어: PageRequestDto 설계
문제 발견
카카오테크캠퍼스에서 선물하기 API에 페이지네이션을 구현하다가 궁금한 점이 생겼다.
1
GET /api/wishlist?page=0&size=3&sort=product.name,desc
이런 API에서 사용자가 악의적인 입력을 넣으면 어떻게 되지?
sort에 이상한 값을 넣으면?sort에 정렬 기준을 엄청 많이 넣으면?size=100000을 넣으면?
Spring의 기본 Pageable은 이런 입력을 그대로 받아들인다.
멘토님한테 질문
멘토님한테 물어봤다.
“PropertyReferenceException을 catch하기엔 예외 범위가 너무 넓고, Pageable.getSort()로 하나하나 체크하는 게 최선인가요? 또, 악의적으로 많은 정렬 기준을 넣으면 문제가 생기지 않나요?”
멘토님 답변:
“아주 좋은 고민이네요! 별도의 PageRequestDto를 만들어서 custom하게 처리하는 게 가장 쉬울 것 같아요.
- sortProperty를 enum으로 관리하고, 유효하지 않으면 에러를 뱉거나 default sort property를 쓰면 됨
- pageSize도 별도의 maxSize(예: 100)를 정해두고, 최대 maxSize만큼만 허용
- 이렇게 하면 validation, 디폴트 처리, 에러 메시지를 간단하게 처리할 수 있어요”
해결: PageRequestDto 설계
SortField 인터페이스
정렬 가능한 필드를 enum으로 관리하기 위한 공통 인터페이스다.
1
2
3
public interface SortField {
String getFieldName();
}
도메인별 정렬 필드 enum
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public enum WishlistSortField implements SortField {
CREATED_AT("createdAt"),
PRODUCT_NAME("product.name"),
PRODUCT_PRICE("product.price");
private final String fieldName;
WishlistSortField(String fieldName) {
this.fieldName = fieldName;
}
@Override
public String getFieldName() {
return fieldName;
}
}
PageRequestDto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public record PageRequestDto(
@Min(value = 1, message = "페이지 번호는 1 이상이어야 합니다.")
Integer page,
@Range(min = 1, max = 100, message = "페이지 크기는 1 이상 100 이하여야 합니다.")
Integer size,
String sortBy,
Boolean ascending
) {
public <T extends Enum<T> & SortField> PageRequest toSafePageable(
Class<T> enumClass, T defaultSortField) {
int page = this.page != null ? this.page : 1;
int size = this.size != null ? this.size : 10;
T sortField = defaultSortField;
boolean ascending = this.ascending != null ? this.ascending : true;
if (sortBy != null) {
for (T enumValue : enumClass.getEnumConstants()) {
if (enumValue.getFieldName().equals(sortBy)) {
sortField = enumValue;
}
}
}
return PageRequest.of(page - 1, size,
Sort.by(
ascending ? Sort.Direction.ASC : Sort.Direction.DESC,
sortField.getFieldName()
)
);
}
}
사용 예시
1
2
3
4
5
6
7
8
9
@GetMapping
public Page<WishlistItemDto> getWishlistItems(
@Valid PageRequestDto pageRequest) {
Pageable pageable = pageRequest.toSafePageable(
WishlistSortField.class,
WishlistSortField.CREATED_AT
);
return wishlistService.getItems(pageable);
}
방어 항목 정리
| 공격 | 방어 |
|---|---|
size=100000 | @Range(max = 100)으로 최대값 제한 |
sort=malicious_field | enum에 없으면 무시, 기본값 사용 |
sort 파라미터 다수 전달 | 단일 sortBy만 받아서 처리 |
page=-1 | @Min(1)로 최소값 제한 |
배운 점
- Spring의 기본 Pageable은 검증 없이 그대로 받아들인다
- 악의적인 입력을 막으려면 커스텀 DTO가 필요하다
- 보안은 설계 단계에서부터 고려해야 한다
카카오테크캠퍼스 3기 선물하기 API 클론 코딩 중 멘토님 피드백 정리.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.