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 if a user sends malicious input?

  • What if they put garbage in sort?
  • What if they spam tons of sort criteria?
  • What if they send size=100000?

Spring’s default Pageable just accepts all of this as-is.


Asked My Mentor

I asked:

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

Mentor’s response:

“Great question! Creating a custom PageRequestDto would be the easiest approach:

  • Manage sortProperty as an enum; throw an error or fall back to default if invalid
  • Set a maxSize (e.g., 100) for pageSize; cap it there
  • This way you get clean handling for validation, defaults, and error messages”

Solution: PageRequestDto Design

SortField Interface

A 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) caps the value
sort=malicious_fieldIgnored if not in enum; falls back to default
Multiple sort paramsOnly single sortBy accepted
page=-1@Min(1) enforces minimum

Lessons Learned

  • Spring’s default Pageable doesn’t validate input
  • You need a custom DTO to block malicious input
  • Security should be baked in 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.