Skip to main content
Laravel, shipping fast.

Weak tokens, tokens that never expire, tokens that leak, and no limit on guessing.

Here: a token in a log file. A token in a URL, where it lands in every proxy’s access log. A token issued three years ago for a service nobody remembers. A client trying tokens as fast as the server will answer.

What stops it: Sanctum generates long random tokens and stores only their hashes. Tokens travel in a header, never a query string, and only over HTTPS. Chapter 4 passes an expiry on every token and sets expiration in the config as a backstop, and it never issues *. The request log records the token’s ID and never the token. Guessing is capped in front of the application, by address, as Chapter 6 arranged.

How you know: search your logs for the token prefix from Chapter 4. It should appear nowhere. List the tokens that haven’t been used in ninety days: each is either a service that died or a credential waiting to be found.

One check is worth doing by hand. Issue a token, delete it, and call the API with it. The answer must be 401 at once. If you ever add caching in front of authentication, this is the test that tells you what you gave up.

3. Broken Object Property Level Authorization

The caller is allowed to touch the record, but not that field. Two directions: reading a property it shouldn’t see, and writing one it shouldn’t set.

Here, reading: settings on a consumer, which holds a provider token, and webhook_secret.

Here, writing: a consumer sending "rate_limit": 100000 or "settings": {...} in a PATCH to its own record.

What stops it: on the way out, the Resource. ConsumerResource names the fields that leave, and neither secret is among them. The model marks both Hidden as a second lock. On the way in, the FormRequest and Fillable: validated() contains only fields that have rules, and neither secret is fillable. #[FailOnUnknownFields] from Chapter 5 goes further and turns the attempt into a 422.

Look closely at rate_limit and license_limit, though. They are in UpdateConsumerRequest, because an operator needs to change them. If an ordinary consumer could ever reach update on its own record, it could raise its own limits. Today update requires the operator’s ability, so the hole is closed. But it is closed by an attribute on the controller and not by the request, and that is the kind of thing a review exists to notice. The day someone lets consumers edit their own contact address, the request class has to split in two.

How you know: the assertJsonMissingPath('data.settings') test from Chapter 5, and its mirror for writing: send a forbidden field and assert that the database didn’t change. (Laravel documentation: Eloquent: Getting Started › Mass Assignment.)

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