A Laravel security audit is neither a generic scanner run nor a linear pass through the OWASP Top 10. It connects attack surface, framework configuration, business logic, source code, data flows and production operations. The goal is not the longest findings list. It is a dependable answer to three questions: what is realistically exploitable, which shared root cause produces further variants and which test prevents recurrence?
A useful audit follows trust boundaries and data, not only suspicious function names.
This checklist is written for team leads and Laravel developers assessing an existing system or preparing an external review. 10 common security mistakes in Laravel projects provides the technical pillar article for individual failure patterns and secure implementation examples.
Define audit goal, depth and evidence
Before running a tool, give the audit a clear assignment:
- Which applications, APIs, domains, mobile clients and repositories are in scope?
- Is source code available or is this a black-box assessment?
- Which roles, tenants and test data are available?
- Which actions are prohibited, such as load tests or production emails?
- How are critical findings escalated during the assessment?
- Does the audit cover only one release or also deployment and supply chain?
A source-code security review discovers different weaknesses from an external penetration test. Both are valuable, but the terms should not be used interchangeably. A combined engagement can validate code-derived hypotheses safely against the running system.
Outcome: A scope inventory, rules of engagement, test accounts, communication path and definition of reproducible evidence.
Inventory the complete attack surface
Laravel applications contain more entry points than routes/web.php:
- web, API and console routes,
- broadcast channels and WebSockets,
- Livewire or Inertia actions,
- webhooks and OAuth callbacks,
- queued jobs and schedulers,
- storage downloads and temporary URLs,
- health, debug and monitoring endpoints,
- separate administration, partner and mobile backends.
A first local overview:
php artisan route:list --except-vendor
php artisan about --only=environment
composer audit --locked
composer outdated --direct
The output is a starting point, not a result. Add dynamically registered routes, subdomain routing, package endpoints and infrastructure rules. Equally important is which guard, middleware and rate limit applies at every entry point.
$routes = collect(Route::getRoutes())->map(fn (\Illuminate\Routing\Route $route) => [
'methods' => $route->methods(),
'uri' => $route->uri(),
'name' => $route->getName(),
'middleware' => $route->gatherMiddleware(),
]);
Review: Look for state-changing GET routes, exposed internal endpoints, inconsistent middleware groups and actions hidden only in the interface.
Review production configuration and deployment
Configuration mistakes can bypass otherwise sound application controls. At minimum, examine:
| Area | Security question |
|---|---|
| Debugging | Is APP_DEBUG guaranteed to be disabled in production? |
| Keys | Are APP_KEY, OAuth, webhook and API secrets separated and rotatable? |
| Cookies | Are domain, Secure, HttpOnly and SameSite correct? |
| Proxy | Are only known proxies and hosts trusted? |
| CORS | Are origins, methods and credentials narrowly constrained? |
| Storage | Are confidential files outside the public web root? |
| Logs | Are tokens, cookies and personal fields redacted? |
| Workers | Do queues load new configuration after deploy and secret rotation? |
Security-critical requirements should fail during boot or a dedicated deployment check:
final class ProductionSecurityConfiguration
{
public static function assertValid(): void
{
if (! app()->isProduction()) {
return;
}
throw_if(config('app.debug'), RuntimeException::class, 'APP_DEBUG is enabled.');
throw_unless(config('session.secure'), RuntimeException::class, 'Secure cookies are disabled.');
throw_unless(filled(config('services.billing.webhook_secret')), RuntimeException::class, 'Webhook secret is missing.');
}
}
Run this check during build and release. Discovering a missing key on the first customer request is too late.
Assess dependencies and supply chain
composer audit identifies known advisories in the lockfile. It does not tell you whether a package is abandoned, an installation script is dangerous or a newly released version is compromised. Avoiding security updates indefinitely is dangerous too.
An audit therefore checks that:
composer.lockis versioned and deployments install reproducibly from it,- direct and transitive packages are visible,
- Composer scripts, GitHub Actions and external build actions are pinned transparently,
- dependencies with access to authentication, files or serialisation receive extra scrutiny,
- updates include tests, review and controlled provenance,
- AI-generated package names are never installed without verification.
composer show --direct
composer audit --locked
composer validate --strict
npm audit
Review: A green advisory scan is not proof of a secure supply chain. Document provenance, update process and privileged dependencies.
Test authentication as a lifecycle
Inventory registration, login, SSO, API tokens, password reset, email change, MFA, recovery, impersonation and logout. Test enumeration, rate limits, token expiry, single use, session rotation and revocation for every path.
Important questions include:
- Do other sessions remain active after a password reset?
- Can a pending email be used for recovery or roles before verification?
- Does replacing an MFA factor require recent confirmation?
- Does a magic link work in another prepared session or for another user?
- Can a suspended account continue through the API or broadcast channels?
The full implementation guide is Secure Laravel authentication and session management.
Trace authorisation beyond roles
Authentication answers only who is making a request. For every protected object, the audit must ask:
- Does the object belong to the expected tenant?
- May this user perform this action?
- Does the current business state allow the action?
- Does the same decision protect HTML, API, export, download and queue paths?
A global role check is usually too broad:
// Too broad for a multi-tenant system.
return $user->role === 'manager';
// The policy includes context, relationship and action.
return $user->tenant_id === $invoice->tenant_id
&& $user->can('invoice.approve')
&& $invoice->status === InvoiceStatus::Pending;
Negative tests deliberately cross users, tenants, objects and states. Separating tenant isolation and authorisation in Laravel covers the architecture in depth.
Follow input to security-sensitive sinks
Validation does not automatically create safety. What matters is where a value ends up. During review, trace input to:
- raw SQL and dynamic columns,
- raw Blade output and HTML sanitisers,
- shell commands and process execution,
- file paths and downloads,
- HTTP requests and redirect destinations,
- headers, email templates and log fields,
- deserialisation and dynamic classes,
- AI tools with external side effects.
Fast search patterns help generate hypotheses:
rg -n "whereRaw|orderByRaw|selectRaw|DB::raw" app
rg -n "\{!!|shell_exec|proc_open|Process::run" app resources
rg -n "Http::|Storage::disk|temporarySignedRoute" app
rg -n "withoutGlobalScope|unguard|guarded = \[\]" app
Every result requires context. A static string in selectRaw() is not automatically vulnerable. A convenient helper may still pass user input into a file path or host.
Treat Livewire and client-synchronised state as input
Public Livewire properties look like normal PHP fields on the server but can be modified by the client. Removing an input field or omitting wire:model does not make the property trustworthy.
use Livewire\Attributes\Locked;
use Livewire\Component;
final class EditInvoice extends Component
{
#[Locked]
public int $invoiceId;
public string $reference = '';
public function save(): void
{
$invoice = Invoice::findOrFail($this->invoiceId);
$this->authorize('update', $invoice);
$validated = $this->validate([
'reference' => ['required', 'string', 'max:100'],
]);
$invoice->update($validated);
}
}
#[Locked] prevents the client from changing this property in Livewire 4, but it does not replace a policy: server-side component code can still change it, and an unchanged value may still be disallowed by business rules. Public properties and action parameters should be treated as untrusted input. Hydration, model binding and nested state need deliberate manipulation tests.
Test files, URLs and integrations separately
Uploads and server-side URL fetching connect the application to untrusted content and remote systems. The audit reviews:
- real size and type limits rather than file extensions alone,
- generated storage names and private storage,
- authorisation on every download,
- SVG, HTML, Office and archive handling,
- image transformation and parser isolation,
- SSRF controls for previews, imports, webhooks and AI agents,
- redirect, DNS, IP and response-size rules,
- network egress as a second protection layer.
An allowlist of known integrations is stronger than a validator for arbitrary URLs:
$integration = Integration::query()
->where('tenant_id', $request->user()->tenant_id)
->whereKey($request->integer('integration_id'))
->firstOrFail();
abort_unless($integration->isProvisionedAndAllowed(), 403);
$response = Http::baseUrl($integration->validated_base_url)
->connectTimeout(2)
->timeout(5)
->withoutRedirecting()
->get('/status');
validated_base_url must be checked server-side against allowed hosts and network ranges when the integration is provisioned; the field name itself is not a control. withoutRedirecting() is deliberate here. Laravel's HTTP client can limit or disable redirects, but it does not automatically validate redirect destinations as an SSRF control. If the integration must follow redirects, validate every new destination against the allowed hosts and IP ranges before making the next request.
Review asynchronous and hidden data paths
Queued jobs, events, notifications, broadcast channels, exports, search and caches execute outside the obvious controller. Common findings include:
- a job loads an object by global ID only,
- permission is checked at dispatch but not execution,
- a cache key omits tenant or role,
- a broadcast channel checks user ID but not account state,
- an export deliberately removes a global scope,
- a notification renders sensitive data after its recipient was suspended.
Broadcast::channel('tenant.{tenant}.invoice.{invoice}', function (User $user, Tenant $tenant, Invoice $invoice): bool {
return $user->is_active
&& $user->tenant_id === $tenant->id
&& $invoice->tenant_id === $tenant->id
&& Gate::forUser($user)->allows('view', $invoice);
});
Review: Select one business security rule and trace it across HTTP, API, queues, broadcasts, cache, search and file storage.
Attack business logic and concurrency
Scanners rarely detect a voucher that can be redeemed twice, an approval limit bypassed through parallel requests or an expired state reactivated by an unusual action sequence.
The audit models:
- allowed state transitions,
- value and quantity limits,
- ordering of dependent actions,
- repeated and concurrent requests,
- idempotency of external events,
- role changes between queue and execution,
- differences between preview and actual mutation.
A database transaction helps only when the relevant row is locked and the invariant is checked again inside the same transaction. Unique constraints provide an important final boundary.
Make findings reproducible and useful to developers
A useful finding contains:
- affected component and trust boundary,
- prerequisites and exact attack path,
- minimal reproducible evidence,
- technical and business impact,
- shared root cause and likely variants,
- specific remediation at architecture and code level,
- a proposed regression test,
- priority considering real reachability.
CVSS helps compare technical severity. Remediation order also requires data classification, exposure, existing controls and business effect. A high score without a reachable path may sit behind a medium-scored, publicly reliable tenant data leak.
Compact Laravel security audit checklist
| Review area | Minimum evidence |
|---|---|
| Scope | Routes, hosts, roles, repositories and exclusions documented |
| Deployment | Debug, secrets, cookies, proxies, CORS and workers reviewed |
| Supply chain | Lockfiles, advisories, privileged packages and actions reviewed |
| Authentication | Login, recovery, MFA, sessions and revocation tested negatively |
| Authorisation | Cross-user, cross-tenant and state cases tested |
| Input | SQL, HTML, processes, paths, URLs and logs traced |
| Files | Type, size, transformation, storage and download authorised |
| Async paths | Jobs, broadcasts, events, search, cache and exports reviewed |
| Business logic | Replay, ordering, limits and concurrency tested |
| Operations | Monitoring, rotation, backup, restore and incident path reviewed |
| Report | Reproduction, impact, cause, fix and regression test included |
When should you use a review, pentest or both?
- Code review: useful for complex permissions, data flows and framework-specific risks.
- Penetration test: useful for the genuinely reachable attack surface and verified exploit chains.
- Combination: useful for business-critical platforms because code hypotheses can be confirmed on the running system and black-box observations traced to their root cause.
- Continuous assessment: useful for frequent releases, many internet-facing assets or integrations that change regularly.
Larger assessments are delivered through DSecured. The development connection matters because a finding creates lasting value only when a team can repair it safely and preserve that fix through tests.
Related Laravel security articles
- 10 common security mistakes in Laravel projects
- Secure Laravel authentication and session management
- Separating tenant isolation and authorisation in Laravel
Sources and further reading
- Laravel 13: Authorization
- Laravel 13: Deployment
- Laravel 13: HTTP Client
- Livewire 4: Security
- OWASP Laravel Cheat Sheet
- OWASP Web Security Testing Guide
- OWASP Application Security Verification Standard
- Securing Laravel: In Depth Articles
- Securing Laravel: Consider All Routes
- Securing Laravel: Validate Config at Boot
Conclusion
A dependable Laravel security audit begins with system boundaries and does not end at the first finding. It connects external reachability, source code, framework behaviour, business logic and operations. The real quality signal is a reproducible attack chain, a root-cause correction and a negative test that preserves the same security invariant in every future release.