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
| Attack | Defense |
|---|---|
size=100000 | @Range(max = 100) limits max value |
sort=malicious_field | Ignored if not in enum, uses default |
Multiple sort params | Only 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.