포스트

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_fieldenum에 없으면 무시, 기본값 사용
sort 파라미터 다수 전달단일 sortBy만 받아서 처리
page=-1@Min(1)로 최소값 제한

배운 점

  • Spring의 기본 Pageable은 검증 없이 그대로 받아들인다
  • 악의적인 입력을 막으려면 커스텀 DTO가 필요하다
  • 보안은 설계 단계에서부터 고려해야 한다

카카오테크캠퍼스 3기 선물하기 API 클론 코딩 중 멘토님 피드백 정리.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.