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
| Attack | Defense |
|---|---|
size=100000 | @Range(max = 100) caps the value |
sort=malicious_field | Ignored if not in enum; falls back to default |
Multiple sort params | Only 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.