Web Application Security: A Practical OWASP Top 10 Guide
SQL injection, broken access control, XSS, mass assignment, and misconfiguration — the real vulnerabilities developers actually cause, with fixes.
An API with no rate limiting is one bad actor (or one buggy client retrying in a loop) away from taking down your entire service. Combined with solid authentication, rate limiting is one of the highest-leverage things you can add to any public-facing API.
Route::middleware('throttle:60,1')->group(function () {
Route::apiResource('posts', PostController::class);
});
throttle:60,1 allows 60 requests per 1 minute, per authenticated user (or per IP for guests). Exceeding it returns a 429 Too Many Requests automatically — no custom code required.
// RouteServiceProvider
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->ip()); // stricter — brute-force protection
});
A login endpoint deserves a much stricter limit than a general read endpoint — 5 attempts per minute meaningfully slows down credential-stuffing attacks without noticeably affecting a real user who occasionally mistypes their password.
// Sanctum — simple token auth for a first-party SPA or mobile app
$token = $user->createToken('mobile-app')->plainTextToken;
// Passport — full OAuth2, needed when THIRD-PARTY apps need to access your API
// on a user's behalf, with the user explicitly granting scoped permission
Most APIs need Sanctum, not a full OAuth2 server — Passport's added complexity only pays for itself when genuinely external applications need delegated access, not when you're just authenticating your own first-party clients.
$token = $user->createToken('read-only-integration', ['posts:read']);
// In a controller or middleware
if (!$request->user()->tokenCan('posts:write')) {
abort(403);
}
A token issued for a read-only reporting integration shouldn't be capable of deleting data even if it leaks — scoped abilities are the mechanism that enforces that, rather than relying on "the integration would never try to do that."
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 42
Retry-After: 30
Laravel adds these automatically on throttled routes — a well-behaved client uses them to back off proactively rather than hammering the API and repeatedly hitting 429s.
A production API often needs more nuance than one global limit — a search endpoint hitting Elasticsearch might need a lower limit than a simple cached-lookup endpoint, and paid-tier customers might get a materially higher limit than free-tier ones. Laravel's RateLimiter::for() supports exactly this kind of per-route, per-user-tier configuration without custom middleware.
SQL injection, broken access control, XSS, mass assignment, and misconfiguration — the real vulnerabilities developers actually cause, with fixes.