Skip to main content
Laravel, shipping fast.
Chapter 19 · A Security Review

4. Unrestricted Resource Consumption

Julian Beaujardin

A caller makes your API do expensive work without limit. Not always an attack. Often a loop with a bug in it.

Here: a list with per_page=1000000, or per_page=-1. A bulk request with ten thousand items. An export of everything, requested every minute. A request body of fifty megabytes.

What stops it: every input in this API has a bound.

| Input | Bound | Chapter | |-------|-------|---------| | Requests per minute | per consumer | 6 | | Requests per second | per address, at the web server | 6 | | per_page | 1 to 100 | 5 | | search | 100 characters | 5, 18 | | webhook_url | 255 characters | 18 | | Items in a bulk request | 100 | 2 | | since on an export | 90 days back | 18 | | Exports | one per consumer every ten minutes | 18 | | contact_email | 254 characters | 5 | | Idempotency-Key, X-Trace-Id | a UUID or nothing | 8, 7 | | Request body | the web server, and PHP’s post_max_size | | | Provider call | 10 seconds, 3 attempts | 3 |

How you know: go through every FormRequest and look for a rule without an upper bound. A string with no max. An array with no max. An integer with no between. Each one is an input whose cost the caller decides. Then do the same for headers your own middleware reads, which no FormRequest covers.

One bound is still missing from this API, and a review should say so. A provider’s response has no size limit. Statamic is trusted not to send a gigabyte, and the ten-second timeout is the only thing that would stop it.

5. Broken Function Level Authorization

The caller reaches an operation it shouldn’t have: an ordinary consumer calling an operator’s endpoint.

Here: a read-only consumer calling DELETE /api/licenses/{license}, or creating a hundred licenses through the batch endpoint. Any consumer calling POST /api/consumers. A consumer issuing a token with more abilities than it holds.

What stops it: every controller declares who may call it. LicenseController, LicenseBatchController, and ExportController require a license ability. ConsumerController and TokenController require the operator’s. And HeldByCaller in Chapter 5 means a caller can grant only abilities it has.

The weakness is the word “every.” A controller that declares nothing is protected only by auth:sanctum, which every consumer passes. Laravel can’t know that you forgot. That is exactly how the batch endpoint in the draft of Chapter 18 let read-only tokens write.

How you know: the table from Chapter 12, with one row per route and per caller who must be refused, run as a test. It is tedious to write. It is also the most valuable page a review produces. A table can’t fail for a route nobody listed, so add the row in the same pull request as the route, and make that a line in your review checklist. (Laravel documentation: Sanctum › Token Abilities.)

The audio could not be loaded. Try again in a moment.