Skip to main content
Blog

Web

Laravel project security: 12 checks before the expertise

APP_DEBUG, trustProxies, CSRF, mass assignment, policies, rate limiting and more — a practical 12-item list for preparing a Laravel app for the expertise.

· 9 min read

Photo: Negative Space, CC0 · Wikimedia Commons

Laravel ships with good security defaults: CSRF protection, password hashing, encrypted cookies. Yet most of the issues an expertise finds come not from the framework itself but from how it is configured. Below are 12 items we check in our own projects and from expertise experience. Examples follow the Laravel 11/12 structure.

1. APP_DEBUG and APP_ENV

In production, the .env file should contain:

APP_ENV=production
APP_DEBUG=false

With APP_DEBUG=true, error pages show stack traces, file paths and sometimes environment variables. To check, request a non-existent route or send an invalid parameter and confirm the response reveals no internal details.

2. trustProxies and the Host header

If the app runs behind Nginx or a load balancer, it has to trust proxy headers — but not all of them. In our own product, the expertise found Host header injection: the app trusted X-Forwarded-Host and could build password reset links using a domain supplied by an attacker. The fix is to trust only the headers you need:

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->trustProxies(at: '127.0.0.1', headers:
        Request::HEADER_X_FORWARDED_FOR |
        Request::HEADER_X_FORWARDED_PORT |
        Request::HEADER_X_FORWARDED_PROTO);
})

In addition, set URL::forceRootUrl(config('app.url')) and URL::forceScheme('https') in AppServiceProvider for production.

3. CSRF exceptions

CSRF protection is on by default, but the validateCsrfTokens(except: [...]) list tends to grow. Review every exception: it should exist only for external service callbacks (a payment provider, for example), and that route must verify a signature or perform another check.

4. Mass assignment

$guarded = [] or Model::unguard() is a warning sign. If a user adds is_admin=1 or balance=... to a request, it may be written to the model. List only permitted fields in $fillable and use $request->validated() instead of $request->all().

5. Authorization: Policies and Gates

Authentication answers "who?", authorization answers "are they allowed to?". Can a user who opened /invoices/15 also open /invoices/16? That is IDOR (CWE-639). Write a Policy for every resource and call $this->authorize('view', $invoice) or Gate::authorize() in the controller. Scope list queries to the current user or organisation as well.

6. Business logic and state checks

On our platform the expertise recorded two business logic findings. The first was a "payment successful" page shown without confirmation from the payment provider. The right approach: change payment status only via the provider's server-to-server callback or an API query, and have the success page display the real status from the database. The second was performing an action without checking state. On every transition (for example "draft → submitted"), verify the current state on the server.

7. Rate limiting

Put limits on login, password reset, OTP sending and search:

RateLimiter::for('login', function (Request $request) {
    return Limit::perMinute(5)->by(
        Str::lower($request->input('email')).'|'.$request->ip()
    );
});

Add ->middleware('throttle:login') to the route, and define a separate limit for API routes.

config/session.php and .env:

SESSION_SECURE_COOKIE=true
SESSION_HTTP_ONLY=true
SESSION_SAME_SITE=lax
SESSION_ENCRYPT=true

Check that $request->session()->regenerate() is called after login, and invalidate() plus regenerateToken() on logout. Laravel starter kits do this, but custom login flows often forget.

9. Dependency audit

composer audit
npm audit --omit=dev

If the output lists packages with known vulnerabilities, update them. Keep composer.lock in the repository and install in production with composer install --no-dev --optimize-autoloader.

10. Signed URLs

Do not leave plain IDs in email verification, file download or unsubscribe links. Use URL::temporarySignedRoute('invoice.download', now()->addMinutes(30), [...]) and the signed middleware on the route. Then nobody can edit the link to reach someone else's record.

11. Information disclosure

Two of our expertise findings were of this kind. The first: Ziggy sent the browser the full route list, including admin routes. In config/ziggy.php, expose only the routes you need via only or groups. The second: excess fields in API responses. Use API Resources instead of returning models directly, and review $hidden. Also remove X-Powered-By and the server version from headers.

12. File uploads and exposed directories

  • Validate uploads with mimes and max rules, and where possible store them on a private disk rather than public.
  • The web server root should be public/ only. Verify that .env, storage/logs and .git cannot be opened from a browser.
  • telescope, horizon, phpinfo and debug panels must be disabled in production or restricted to administrators.

A good rule: mark each item not as "it's in the code" but as "I tested it".

How to verify

Reading the list once is not enough. Run a concrete check for each item: request a password reset with curl -H "X-Forwarded-Host: example.org", open a resource with another user's ID, enter a wrong password 10 times in a row. Write the results down — you will need exactly these notes when preparing your response to the expertise.

Summary

In Laravel, security often comes down to a single line of configuration. Going through these 12 items in order closes a large share of typical expertise findings in advance.

Sipar checks Laravel projects against these and other items: a Claude AI agent reviews code and configuration, and an expert confirms the findings and fixes them. To have your project checked, request a security scan.