Post

Defending Against Malicious Pageable Input: PageRequestDto Design

Defending Against Malicious Pageable Input: PageRequestDto Design

Problem Discovery

While implementing pagination for the Gift API at Kakao Tech Campus, a question came up:

1
GET /api/wishlist?page=0&size=3&sort=product.name,desc

What happens if a user sends malicious input to this API?

  • What if they put weird values in sort?
  • What if they add tons of sort criteria?
  • What if they send size=100000?

Spring’s default Pageable accepts all these inputs as-is.


Asked My Mentor

I asked:

“PropertyReferenceException is too broad to catch, and checking with Pageable.getSort() one by one seems tedious. Also, wouldn’t there be issues if someone maliciously sends many sort criteria?”

Mentor’s response:

“Great question! Creating a custom PageRequestDto for custom handling would be easiest.

  • Manage sortProperty as an enum, throw error or use default if invalid
  • Set a maxSize (e.g., 100) for pageSize, only allow up to maxSize
  • This way you can easily handle validation, defaults, error messages”

Solution: PageRequestDto Design

SortField Interface

Common interface for managing sortable fields as enums.

1
2
3
public interface SortField {
    String getFieldName();
}

Domain-specific Sort Field 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 = "Page number must be at least 1")
        Integer page,

        @Range(min = 1, max = 100, message = "Page size must be between 1 and 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()
                )
        );
    }
}

Usage Example

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);
}

Defense Summary

AttackDefense
size=100000@Range(max = 100) limits max value
sort=malicious_fieldIgnored if not in enum, uses default
Multiple sort paramsOnly single sortBy accepted
page=-1@Min(1) limits min value

Lessons Learned

  • Spring’s default Pageable accepts input without validation
  • Custom DTO needed to block malicious input
  • Security should be considered from the design phase

From Kakao Tech Campus 3rd cohort Gift API clone coding, summarizing mentor feedback.

This post is licensed under CC BY 4.0 by the author.