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)로 최소값 제한 |
정리
Pageable은 편하지만, 그대로 노출하면 입력 검증, 에러 메시지, 기본값 정책이 흐려진다- 받을 수 있는 값의 범위를 명확히 정해두고(
size,sortBy), 그 밖은 기본값이나 에러로 통제하는 게 운영에 유리했다 - 이런 방어는 거창한 보안 기능이라기보다, API 계약을 선명하게 만드는 작업에 가깝다
프로젝트: 카카오테크캠퍼스 3기 선물하기 API 클론 코딩 관련 링크
- GitHub: Neibce/spring-gift-wishlist
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.