Laravel Security Guide: Supply-Chain Risks, Composer, and Application Scanning

Laravel API development is the fastest way to ship a reliable, secure backend for a SPA, mobile app, or third-party integration. Laravel 12 (released March 2025, PHP 8.2 minimum) provides tools like Sanctum for authentication, Eloquent Resources for response shaping, native CORS handling, and Redis-backed rate limiting. Correct integration is crucial. This guide covers every layer of a production-grade API: scaffolding, authentication, response design, versioning, rate limiting, queue workers, security, and testing. Before starting, review our Laravel security guide for securing your Laravel application at the infrastructure level.
Whether you're a solo developer in Mumbai building a client portal or an engineering team shipping a multi-tenant SaaS product, the patterns in this guide will help you avoid common production failures.
Need a production-ready Laravel API built for your Mumbai business? Our team has shipped APIs for SaaS products, ecommerce platforms, and enterprise portals — with full test coverage and security-first architecture.
Discuss Your Laravel ProjectPrerequisites and Version Requirements
Laravel 12 requires PHP 8.2 or higher. PHP 8.3 and 8.4 are supported, but PHP 8.5 compatibility was incomplete as of early 2026 — verify before upgrading. Required PHP extensions include Ctype, cURL, DOM, Fileinfo, Filter, Hash, Mbstring, OpenSSL, PCRE, PDO, Session, Tokenizer, and XML. Laravel 11 still receives security patches, making it a valid choice for existing codebases.
Composer 2.x, a Redis instance (for rate limiting, queues, and caching in production), and a MySQL or PostgreSQL database are necessary. File-based drivers are suitable for local development but do not scale under concurrent API load.
- Laravel 12.x — current stable (March 2025); bug fix support for 18 months, security fixes for 2 years
- Laravel 11.x — still receiving security patches; minimum PHP 8.2
- Laravel Passport — upgrade to 13.7.1+ if using 13.x (CVE-2026-39976 patched)
- Laravel Livewire — upgrade to 3.6.4+ (CVE-2025-54068, CVSS 9.8)
Scaffolding Your API
Laravel API scaffolding in Laravel 11 and 12 is opt-in. A fresh laravel new install does not include routes/api.php by default. The most common mistake is defining API routes before running the scaffold command, leading to 404 errors.
Run this command first:
# Install API scaffolding — creates routes/api.php, installs Sanctum php artisan install:apiTo use Passport instead (OAuth2 server flows only)
php artisan install:api --passport
This command creates routes/api.php, publishes Sanctum's configuration, adds the throttle:api middleware to the API route group, and runs the Sanctum migrations. Do not skip it.
API Versioning From Day One
URL-based versioning (/api/v1/, /api/v2/) is the most widely adopted strategy. Header-based versioning (Accept header) keeps URLs cleaner but is harder to debug in browsers and harder to route in middleware. Use URL versioning unless your team has a strong reason otherwise.
Set up separate route files per version:
// routes/api.php — versioned route groups use Illuminate\Support\Facades\Route;
Route::prefix('v1')->group(base_path('routes/api_v1.php')); Route::prefix('v2')->group(base_path('routes/api_v2.php'));
Keep separate controller namespaces — App\Http\Controllers\Api\V1 and App\Http\Controllers\Api\V2 — and separate Eloquent Resource classes per version. This lets V1 and V2 evolve independently without breaking existing consumers. Retrofitting versioning after launch is expensive; the cost of not versioning from day one is far higher than the minor overhead of setting it up now.
Authentication: Sanctum vs Passport
Laravel Sanctum handles 95% of API authentication needs. It supports SPA cookie-based authentication (HttpOnly cookies, CSRF protection) and API token authentication (simple opaque tokens stored in the database) without OAuth2 complexity. Sanctum is installed by php artisan install:api and requires no additional configuration for most projects.
Use Passport only when you are building a full OAuth2 authorization server — specifically when third-party applications need to request access on behalf of your users via authorization_code or client_credentials grant flows. Passport adds migrations, encryption key management, and client management overhead that is completely unnecessary for a first-party SPA or mobile app.
| Scenario | Sanctum | Passport |
|---|---|---|
| Own SPA consuming your API | ✓ Recommended | Overkill |
| Own mobile app | ✓ Recommended | Overkill |
| Simple API tokens for integrations | ✓ Recommended | Overkill |
| Third-party OAuth2 authorization_code flow | Not supported | ✓ Required |
| Machine-to-machine client_credentials | Can work with tokens | ✓ Native support |
Protect API routes with Sanctum's middleware:
// routes/api.php use Illuminate\Http\Request; use Illuminate\Support\Facades\Route;
Route::middleware('auth:sanctum')->group(function () { Route::apiResource('posts', PostController::class); Route::get('/user', fn (Request $r) => $r->user()); });
Token storage matters. For browser-based SPAs, use Sanctum's stateful (HttpOnly cookie) authentication — never store tokens in localStorage, because XSS attacks can exfiltrate them. For mobile apps and CLI clients, use Sanctum API tokens stored in the device's secure keychain.
Eloquent API Resources: Shape Every Response
Eloquent API Resources are the standard transformation layer between your models and JSON. Every API endpoint that returns model data should go through a Resource class. Raw Eloquent model output exposes your internal database column names, timestamps in inconsistent formats, and any sensitive fields that happen to be on the model.
Create a resource:
php artisan make:resource PostResourceThen define the transformation:
// app/Http/Resources/PostResource.php use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource { public function toArray($request): array { return [ 'id' => $this->id, 'title' => $this->title, 'author' => new UserResource($this->whenLoaded('author')), 'comments_count' => $this->whenCounted('comments'), 'created_at' => $this->created_at->toISOString(), ]; } }
Two methods are essential for production use:
whenLoaded('relationship')— includes the relationship only if it was already eager-loaded. Without this, accessing$this->authorinsidetoArray()fires a lazy-load query for every item in a collection — classic N+1.whenCounted('relationship')— includes a count only ifwithCount()was appended on the query. Same principle: nowithCount, no extra query.
The $wrap property defaults to 'data'. If existing API consumers expect unwrapped responses, set public static $wrap = null or call ->withoutWrapping() on collection responses. Make this decision explicitly — changing it after consumers are built breaks their parsing.
Preventing N+1 Queries in Controllers
N+1 queries are the single most common performance killer in Laravel APIs. A collection of 50 posts that each lazily loads an author relationship fires 51 database queries instead of 2. Under production load, this degrades response times from tens of milliseconds to seconds.
The fix is always eager loading. In your controller:
// app/Http/Controllers/Api/V1/PostController.php use App\Http\Resources\PostResource; use App\Models\Post; use Illuminate\Http\Resources\Json\AnonymousResourceCollection;public function index(): AnonymousResourceCollection { $posts = Post::with(['author', 'tags']) ->withCount('comments') ->paginate(20);
return PostResource::collection($posts);
}
In non-production environments, enforce eager loading at the model level to catch violations early:
// app/Providers/AppServiceProvider.php use Illuminate\Database\Eloquent\Model;
public function boot(): void { Model::preventLazyLoading(! app()->isProduction()); }
For a comprehensive look at query optimization patterns, including cursor pagination, raw queries for bulk reads, and index strategies, see our guide on Eloquent optimization.
The beyondcode/laravel-query-detector package is an alternative that throws exceptions or logs warnings for N+1 queries in local and staging environments. Use either approach — the key is that N+1 violations surface before they reach production.
Form Requests, Validation, and Mass Assignment Safety
When an XHR or API request triggers a validation failure, Laravel 12 automatically returns a 422 JSON response with all validation errors — no extra configuration needed. You can customize the error envelope by overriding failedValidation() in a FormRequest class:
// app/Http/Requests/StorePostRequest.php use Illuminate\Foundation\Http\FormRequest; use Illuminate\Contracts\Validation\Validator; use Illuminate\Http\Exceptions\HttpResponseException;class StorePostRequest extends FormRequest { public function authorize(): bool { return true; }
public function rules(): array { return [ 'title' => ['required', 'string', 'max:255'], 'content' => ['required', 'string'], ]; } protected function failedValidation(Validator $validator) { throw new HttpResponseException( response()->json([ 'success' => false, 'errors' => $validator->errors(), ], 422) ); }
}
Always pass $request->validated() to model create/update calls — never $request->all(). The difference: validated() returns only the fields declared in your rules() array; all() returns every field the client sends, including fields that could exploit mass assignment vulnerabilities.
// Correct — only validated fields pass to the model
public function store(StorePostRequest $request): PostResource
{
$post = Post::create($request->validated());
return new PostResource($post);
}
Never define protected $guarded = [] on models that handle user-controlled input from API controllers. Never call forceFill() or forceCreate() on request data. Define $fillable explicitly on every model.
Rate Limiting: Redis-Backed, Production-Safe
Rate limiting in Laravel 11 and 12 is configured in bootstrap/app.php (not RouteServiceProvider as in Laravel 10). The throttle:api middleware applies to all api.php routes by default after running php artisan install:api.
// bootstrap/app.php — custom rate limiter use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Http\Request; use Illuminate\Support\Facades\RateLimiter;
RateLimiter::for('api', function (Request $request) { return $request->user() ? Limit::perMinute(120)->by($request->user()->id) : Limit::perMinute(30)->by($request->ip()); });
This configuration gives authenticated users 120 requests per minute (keyed by user ID to prevent one user's rate limit from affecting others) and unauthenticated visitors only 30 (keyed by IP to slow credential stuffing).
Critical production requirement: use Redis as your cache driver. The file or database cache driver creates race conditions and disk I/O bottlenecks under concurrent API load. Rate limiting via the file driver on a high-traffic endpoint is effectively no rate limiting at all — multiple concurrent requests read the counter before any write commits.
CORS Configuration
Laravel has handled CORS natively since version 9.2 via Illuminate\Http\Middleware\HandleCors. Configure it in config/cors.php:
// config/cors.php — typical API CORS setup
return [
'paths' => ['api/'],
'allowed_methods' => [''],
'allowed_origins' => [env('FRONTEND_URL', 'https://app.example.com')],
'allowed_headers' => ['Content-Type', 'Authorization', 'X-Requested-With'],
'supports_credentials' => true,
'max_age' => 86400,
];
Never set allowed_origins to '*' when supports_credentials is true. Browsers reject credentialed requests (cookies, Authorization headers) to wildcard origins — the combination is both a security misconfiguration and a functional bug. Always enumerate specific allowed origins from environment variables so they differ between staging and production.
Queue Workers and Laravel Horizon
Long-running operations in API handlers — sending emails, processing uploads, calling third-party APIs — must go on a queue. An API endpoint that blocks for 3 seconds while sending a transactional email will fail under concurrent load. Dispatch the job and return 202 Accepted immediately.
Laravel Horizon is the official Redis queue monitoring dashboard. Use named queues with dedicated worker pools:
# Run workers with memory and job limits to prevent drift php artisan horizonOr individual workers
php artisan queue:work redis --queue=critical,default,emails,imports
--max-jobs=500 --max-time=3600
The --max-jobs and --max-time flags are mandatory in production. PHP processes accumulate memory over thousands of iterations. Workers without these flags will eventually exhaust server memory — not immediately, but after days or weeks of running, at the worst possible moment.
Performance: Laravel Octane
Laravel Octane keeps the application bootstrapped in memory between requests, eliminating the bootstrap overhead of every PHP-FPM request cycle. April 2026 benchmarks show RoadRunner delivering approximately 2.1x the throughput of PHP-FPM on typical API workloads. FrankenPHP is recommended for containerised and cloud-native deployments because it ships as a single binary with no PHP extension required.
| Driver | Throughput vs PHP-FPM | Best For | Requirement |
|---|---|---|---|
| RoadRunner | ~2.1x | Pure API throughput | Go binary |
| FrankenPHP | ~1.8–2x | Container / Kubernetes | Single binary, no extension |
| Swoole | ~1x–1.5x | Coroutines / async I/O | C extension required |
Octane requires a careful application audit before enabling. Static properties, singletons, and open file handles can leak state between requests in long-running processes. Any singleton that holds per-request state must be re-bound in the Octane lifecycle hooks. The performance gains are real, but they require deliberate code review.
We build and deploy Laravel APIs with Octane, Horizon, Redis, and full CI/CD pipelines for Mumbai businesses. If you want a scalable API that's production-ready from day one, let's talk.
Explore Custom Web App DevelopmentSecurity Considerations and Known CVEs
Laravel API security requires active maintenance — not just correct initial configuration. The following issues are active in the 2025–2026 timeframe:
- CVE-2026-39976 (CVSS 7.1) — Laravel Passport 13.0.0–13.7.0: client_credentials JWT tokens set
subto the client ID. If a user's integer ID matches the client ID, the TokenGuard authenticates them as that user. Fix: upgrade tolaravel/passport >= 13.7.1. Advisory: GHSA-349c-2h2f-mxf6. - CVE-2025-54068 (CVSS 9.8) — Laravel Livewire up to v3.6.3: unauthenticated RCE via unsafe component property hydration. Fix: upgrade to Livewire >= 3.6.4.
- CISA KEV: CISA flagged a Laravel vulnerability with a patch deadline of April 3, 2026. Verify your Laravel version is current.
Beyond CVE patches, apply these rules universally:
- Set
APP_DEBUG=falseandAPP_ENV=productionin all production.envfiles. Debug mode exposes full stack traces and environment variable values in JSON error responses. - Rotate
APP_KEYbefore deploying to production. Changing it in production invalidates all existing sessions and signed links — do it before you have users. - Run
composer auditregularly. It checks your installed packages against the PHP Security Advisories Database. - Validate file uploads by MIME type using
mimes:or thefilerule — not by file extension alone. Store uploads outside the public directory and serve via signed URLs. - Set tokens with explicit
exp(expiry) claims. Tokens without expiry remain valid permanently, even after compromise. - Use HTTPS exclusively. Set
HTTPSmode on Sanctum cookies in production ('secure' => trueinconfig/sanctum.php). Configure HSTS headers at the web server level.
Testing and Verification
Every production API endpoint needs a feature test that exercises the full HTTP stack — not just unit tests on controllers. Laravel's testing helpers make this straightforward with Pest or PHPUnit:
// tests/Feature/Api/V1/PostTest.php use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase;test('authenticated user can create a post', function () { $user = User::factory()->create();
$response = $this->actingAs($user, 'sanctum') ->postJson('/api/v1/posts', [ 'title' => 'Hello World', 'content' => 'Body text here.', ]); $response->assertStatus(201) ->assertJsonStructure(['data' => ['id', 'title']]);});
test('unauthenticated request returns 401', function () { $this->postJson('/api/v1/posts', ['title' => 'Test']) ->assertStatus(401); });
test('validation failure returns 422 with errors', function () { $user = User::factory()->create();
$this->actingAs($user, 'sanctum') ->postJson('/api/v1/posts', []) ->assertStatus(422) ->assertJsonStructure(['errors' => ['title', 'content']]);
});
Run the suite before every deployment:
php artisan test --parallel
Use RefreshDatabase in feature tests to reset state between runs. Never test against a production database. On CI/CD, configure a separate testing database connection so migrations and seeding are isolated.
Common Mistakes That Break Production APIs
- Not running
php artisan install:apiin Laravel 11/12 —routes/api.phpdoes not exist in fresh installs; routes silently 404. - Using
$request->all()inModel::create()— bypasses$fillableguards and opens mass assignment attacks. - Leaving
APP_DEBUG=truein production — full stack traces and environment variable values leak in JSON error responses. - Not using
whenLoaded()in API Resources — accessing$this->relationshipinsidetoArray()triggers a lazy-load query per resource item. - Rate limiting without Redis — the file or database cache driver creates race conditions under concurrent load.
- Hardcoding
allowed_origins: '*'withsupports_credentials: true— browsers reject this combination and it's a security misconfiguration. - Not versioning from day one — retrofitting
/api/v2/after launch requires coordinating client updates and maintaining dual routing indefinitely. - Storing Sanctum tokens in localStorage — XSS attacks can exfiltrate them; use HttpOnly cookies for browser clients.
- Omitting
--max-jobsand--max-timeon queue workers — PHP processes accumulate memory; workers without these flags eventually exhaust server RAM. - Laravel Passport 13.0.0–13.7.0 with client_credentials — CVE-2026-39976 allows a machine token to authenticate as a real user. Upgrade to 13.7.1+.
- Returning HTML error pages to API clients — happens when the exception handler doesn't detect the
Accept: application/jsonheader. Wrap routes in appropriate middleware or use$request->expectsJson(). - Calling
Model::unguard()globally — disables all mass assignment protection across the entire application.
Architecture Decisions Summary
Every Laravel API project requires conscious choices on these axes. The right answer depends on your scale, team, and deployment environment — but the tradeoffs are well-established:
| Decision | Option A | Option B | Verdict |
|---|---|---|---|
| Authentication | Sanctum | Passport | Sanctum for 95% of projects; Passport only for OAuth2 server |
| API versioning | URL prefix (/v1/) | Accept header | URL versioning — more debuggable and cache-friendly |
| Response shaping | Eloquent Resources | Raw arrays | Always use Resources — conditional fields prevent N+1 |
| Pagination | paginate() | cursorPaginate() | Cursor pagination for large datasets (OFFSET degrades at millions of rows) |
| Performance | PHP-FPM | Octane (RoadRunner/FrankenPHP) | Octane for API-heavy workloads; requires app audit first |
| Queues | Redis driver | Database driver | Redis always in production; database only for dev |
| Read-heavy endpoints | Eloquent | Raw DB queries | Raw queries for bulk exports/reports (2–5x faster) |
Frequently Asked Questions
What is the difference between Laravel Sanctum and Passport for API authentication?
Laravel Sanctum is a lightweight authentication package for first-party SPAs, mobile apps, and simple API tokens. It does not implement OAuth2 and does not require migrations for clients, encryption keys, or grant management. Laravel Passport is a full OAuth2 server implementation needed only when you are issuing tokens to third-party applications via authorization_code or client_credentials grant flows. For most Mumbai businesses building their own API backend, Sanctum is the correct and simpler choice — Passport adds significant operational overhead that is only justified for public-facing OAuth2 providers.
Why does routes/api.php not exist in my new Laravel 11 or 12 project?
Starting in Laravel 11, the routes/api.php file is no longer included in the default project scaffold. Laravel made this change to keep new projects lean — many projects do not need an API. To restore it, run php artisan install:api, which creates the file, installs Sanctum, publishes its configuration, and registers the throttle:api middleware on the API route group. This is a one-time command; run it immediately after creating a new project that will serve API consumers.
How do I prevent N+1 query problems in Laravel API responses?
N+1 queries occur when a collection of models lazily loads a relationship for each item individually, firing one query per item instead of a single eager-load query. The fix is to use Post::with(['author', 'tags'])->withCount('comments')->paginate(20) in your controller, and to use $this->whenLoaded('author') and $this->whenCounted('comments') inside your Eloquent Resource's toArray() method. Add Model::preventLazyLoading(!app()->isProduction()) in AppServiceProvider::boot() to catch violations in development before they reach production.
Why should rate limiting in Laravel use Redis and not the file driver?
The file cache driver stores rate limit counters in files on disk. Under concurrent API requests, multiple processes can read the counter simultaneously before any write commits, creating race conditions where the counter increments incorrectly and the limit is never enforced. Redis uses atomic operations (INCR, EXPIRE) that eliminate this race condition entirely. Redis also handles the I/O throughput of hundreds of concurrent requests without the disk bottleneck that file-based storage creates. In production, always set CACHE_DRIVER=redis in your .env before enabling rate limiting.
What is the best strategy for versioning a Laravel REST API?
URL-based versioning using route prefix groups (/api/v1/, /api/v2/) is the most widely adopted and debuggable strategy for Laravel REST APIs. Keep separate controller namespaces (App\Http\Controllers\Api\V1) and separate Eloquent Resource classes per version so each version can evolve independently without breaking existing consumers. Start versioning from day one — retrofitting versioning after launch requires coordinating client updates across mobile apps, SPAs, and third-party integrations simultaneously, which is a costly and error-prone process. Header-based versioning (Accept header) is an alternative but is harder to test in browsers and harder to route in Laravel middleware.
