Unbounded limit/offset parameters allow resource exhaustion #11
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The
limitandoffsetquery parameters inAdminController,RecipeController, andAdminServicehave no upper bound validation. A user can setlimit=999999to force loading entire tables into memory. Settinglimit=0causesArithmeticExceptioninPageRequest.of(offset / limit, limit).Affected files
AdminController.java:26-27—limitdefaulted to 50 but unboundedRecipeController.java:36-37— sameAdminService.java:45—PageRequest.of(offset / limit, limit)divides bylimitAttack scenario
GET /v1/admin/users?limit=0→ArithmeticException(division by zero) → stack trace leak (see #7)GET /v1/recipes?limit=1000000→ OOM or extreme DB loadRecommended fix
Clamp
limitto1..100range and validateoffset >= 0. Reject invalid values with 400 Bad Request.Severity
Medium — denial of service via resource exhaustion; crash via division by zero.
👨💻 Kai — Frontend Engineer
This is a backend validation issue, but I want to flag where the frontend touches pagination parameters.
Where I send
limitandoffsetlimit=0by accident (e.g., an uninitialized state variable, a misconfigured default), the backend crashes instead of returning a clean error. That's a bad failure mode for my+page.server.tsload functions.Questions for me
limitandoffsetuser-controlled in the UI, or are they fixed constants in my fetch calls? If they're fixed (e.g., alwayslimit=25), the attack vector doesn't go through my UI — it's a direct API caller issue. But the fix is still correct.What I need from the backend fix
400 Bad Requestwith a structured error body whenlimitoroffsetis invalid. Not a 500, not a stack trace (that's issue #7), not an empty response.+page.server.tserror handling can treat all validation failures the same way.1..100as the issue suggests? I'll document that in any API call utilities I write.🛠️ Backend Engineer
Two separate bugs in one issue — worth fixing together but tracking distinctly: a division-by-zero crash and an unbounded resource consumption vector.
Fix for
limit=0(the crash)The root cause is
PageRequest.of(offset / limit, limit)— integer division by zero whenlimit=0. The fix is straightforward: validate before computing.Preferred approach using Bean Validation in the controller:
With
@Validatedon the controller class and a@ExceptionHandler(ConstraintViolationException.class)in a@ControllerAdvice, this produces a clean 400 with no stack trace and no division by zero.Fix for
limit=999999(the resource exhaustion)@Max(100)on thelimitparameter caps it. The question is: is 100 the right ceiling? Depends on the use case:The ceiling doesn't need to be the same for every endpoint — use what makes sense for each resource type.
Questions
PageRequest.of(offset / limit, limit)the right pagination model? This converts an absolute offset to a page number, which is correct but unusual. Spring Data'sPageRequest.of(page, size)expects a 0-indexed page number. Ifoffset=50andlimit=25, this gives page 2 — correct. But ifoffset=30andlimit=25, you get page 1 (integer division), which skips the first 25 items instead of 30. Is this intentional?page+sizeinstead ofoffset+limit? The current approach is offset-based pagination from the API but page-based internally — that mismatch is worth resolving cleanly.AdminControllerthe only place with the division-by-zero bug, or doesRecipeControllerhave the samePageRequest.of(offset / limit, limit)pattern?🧪 QA Engineer
Two distinct failure modes here — each needs its own test, and both are easy to reproduce reproducibly.
Tests for the division-by-zero crash (
limit=0)shouldReturn400WhenLimitIsZero()—GET /v1/admin/users?limit=0→ assert 400, assert no stack trace in response body (see #7).shouldReturn400WhenLimitIsZeroForRecipes()— same forGET /v1/recipes?limit=0.shouldReturn400WhenLimitIsNegative()—limit=-1should also be rejected.Tests for resource exhaustion (
limit=999999)shouldReturn400WhenLimitExceedsMaximum()—GET /v1/recipes?limit=101→ assert 400.shouldReturn400WhenLimitIsExtremelyLarge()—GET /v1/admin/users?limit=999999→ assert 400, assert the request completes quickly (no DB query should execute with this value).shouldReturn200WithMaxAllowedLimit()—GET /v1/recipes?limit=100→ assert 200 (boundary value, valid).shouldReturn200WithMinAllowedLimit()—GET /v1/recipes?limit=1→ assert 200 (boundary value, valid).Tests for
offsetvalidationshouldReturn400WhenOffsetIsNegative()—offset=-1→ assert 400.shouldReturn200WhenOffsetIsZero()—offset=0→ assert 200 (default, valid).Parameterized test opportunity
All the invalid-parameter cases are a great fit for
@ParameterizedTest:Question
@ControllerAdvice/@ExceptionHandlerforConstraintViolationException? If not, the test forlimit=0will currently return a 500, not a 400 — which means we need both the validation annotation fix AND the exception handler before the tests can pass. These might need to land together.🔒 Sable — Security Engineer
Two vulnerabilities in one issue — both exploitable with a single HTTP request, no authentication bypass required.
Vulnerability 1: Denial of Service via
limit=0GET /v1/admin/users?limit=0throwsArithmeticException(division by zero) inAdminService.?limit=0repeatedly would generate repeated exceptions, fill logs, and degrade performance. No authentication needed if the endpoint is accessible to any authenticated user.Vulnerability 2: Resource Exhaustion via
limit=999999usersorrecipes, loads all rows into the JPA entity graph, and serializes them into the HTTP response. On a large dataset, this is an OOM or extreme GC pressure event.SELECT * FROM recipes LIMIT 999999— potentially a full sequential scan.Fix requirements from a security standpoint
PageRequestis constructed.{ "error": "limit must be between 1 and 100" }is fine.Additional questions
GET /v1/admin/usersrequires admin role, the attack surface is smaller — but admin accounts can still be compromised or misused, so validation still matters.limit=0compound issue #7? If #7 is fixed first (stack traces suppressed in prod), the information leakage component of this bug is mitigated — but the crash itself still needs fixing.🎨 Atlas — UI/UX Designer
Backend validation issue, but pagination parameters touch the UI in a few ways worth designing around.
Where pagination appears in the UI
limitandoffset. The defaultlimit=50from the backend is fine for a household-sized recipe collection — but if we ever add a search or filter view, the page size may need to be configurable.Design questions
limitvalue I'd spec — a fixed page size for page-based, a smaller chunk size for infinite scroll.limitat 100, does the UI ever need to show more than 100 items at once? For a household recipe collection, probably not. But for admin views, a household might have hundreds of users or recipes.Error state for invalid pagination
limitand gets a 400 back, what does the user see? An empty list? A generic error? Since this shouldn't happen in normal UI usage, a simple "couldn't load results, try refreshing" toast is probably sufficient.offsetpoints past the end of the results? (e.g., the user is on page 3 but items were deleted.) The API should return an empty page, and the UI should show an empty state — not an error. Is the backend designed to return[]in that case rather than a 404?No design blockers — the fix is pure backend. Just want to make sure error states and pagination UX are defined before Kai implements the list components.