How to Fix an Exposed Laravel Storage Directory

Your Laravel storage directory is publicly accessible, exposing logs, cache files, and uploaded data. Learn how to restrict access.

High severity Application Security Updated 2026-03-01 Markdown

Laravel's storage/ directory is the framework's runtime data store — a catch-all for everything the application generates and needs to persist between requests. This includes framework-compiled files like cached views in storage/framework/views/, bootstrap caches, session files in storage/framework/sessions/ (when using the file driver), application logs in storage/logs/, and everything in storage/app/ including user file uploads. The directory was never intended to be web-accessible and contains no index.php or index.html — but server misconfiguration can make it fully browsable.

The most sensitive component is the log directory. storage/logs/laravel.log accumulates every exception, database query (when verbose logging is enabled), user action, API response, and application error. In practice, Laravel logs routinely contain user email addresses, partial or full request payloads (including form data), database query patterns that reveal your schema, third-party API responses with sensitive fields, and stack traces exposing file paths and package versions. A few hours of logs from a busy application give an attacker a comprehensive picture of how the application works and what data it processes.

Session files are equally sensitive when using the file session driver. A file-based session contains the user's authentication state and session data in serialized form. An attacker who reads session files can potentially reconstruct a valid session cookie and authenticate as any user whose session file is accessible. Automated scanners routinely check for storage/logs/laravel.log and storage/framework/sessions/ as part of their standard fingerprinting of Laravel applications.

The Problem

An exposed storage directory allows attackers to access your application logs (containing error details and potentially sensitive data), session files, cache data, and user-uploaded files that should be private. The storage directory contains framework-generated files and user uploads that were never intended to be publicly accessible. Automated scanners regularly check for exposed Laravel storage paths.

How to Fix

  1. 1

    Verify your document root

    The most common cause of an exposed storage directory is an incorrect document root. Your web server must point to /public, not the project root:
    # Nginx - correct
    root /var/www/yourapp/public;
    # Nginx - WRONG, exposes storage/
    root /var/www/yourapp;
    With the correct document root, the storage directory is outside the web-accessible path and cannot be accessed directly.
  2. 2

    Remove or secure the storage symlink

    The php artisan storage:link command creates a symlink from public/storage to storage/app/public. Only files in storage/app/public should be web-accessible.

    Check what the symlink points to:

    ls -la public/storage

    It should point to ../storage/app/public, not ../storage. If it points to the wrong location, recreate it:

    rm public/storage
    php artisan storage:link

    Never store private files in storage/app/public. Use storage/app/private/ for sensitive uploads.

  3. 3

    Block direct access to storage paths

    Add web server rules to block access to storage directories that should not be public. In Nginx:

    location ~* ^/storage/(app|framework|logs) { deny all; return 404; }

    In Apache .htaccess:
    RewriteRule ^storage/(app|framework|logs) - [F,L]

    This ensures only public/storage (the symlink to storage/app/public) is accessible.

  4. 4

    Serve private files through a controller

    For files that require authentication, serve them through a Laravel controller instead of direct file access:

    Route::get('/files/{path}', function (string $path) {
        $fullPath = storage_path('app/private/' . $path);

    if (!file_exists($fullPath)) { abort(404); }

    // Add authorization check
        // abort_unless(auth()->user()->canAccessFile($path), 403);
    return response()->file($fullPath);
    })->middleware('auth')->where('path', '.*');

How to Verify

Check that storage directories are not accessible:

curl -I https://yourdomain.com/storage/logs/laravel.log
curl -I https://yourdomain.com/storage/framework/sessions/
curl -I https://yourdomain.com/storage/app/

All should return 403 or 404. Only public/storage files (user-uploaded public assets) should return 200.

Prevention

Always use the correct document root. Store sensitive files in storage/app/private/ and serve through authenticated controllers. Never store user uploads in a publicly accessible directory without access controls. Use StackShield to monitor for exposed storage paths.

Frequently Asked Questions

What sensitive data is in the storage directory?

The storage directory contains: application logs with error details and stack traces (storage/logs/), session data for all users (storage/framework/sessions/), compiled Blade templates (storage/framework/views/), cached configuration (storage/framework/cache/), and uploaded files (storage/app/). Logs are especially dangerous as they may contain user data, API responses, and exception details.

Is the public/storage symlink safe?

The symlink from public/storage to storage/app/public is safe by design. Only files you explicitly place in storage/app/public are accessible. The risk comes from storing sensitive files in the public directory or having the wrong document root that exposes the entire storage directory.

How can I quickly check if my storage directory is exposed?

Run: curl -I https://yourdomain.com/storage/logs/laravel.log and curl -I https://yourdomain.com/storage/framework/sessions/. A 200 status code means the files are accessible. Also check curl -I https://yourdomain.com/storage/ — if you see an HTML directory listing or file content rather than a 403/404, your document root is misconfigured.

What should I store in storage/app/public versus storage/app/private?

storage/app/public is for files that are intentionally public and accessible via URL — user avatars, publicly shared documents, thumbnail images. storage/app/ (outside the public subdirectory) is for private files that require authentication to access — invoices, private uploads, sensitive documents. Serve private files through a controller that checks authorization before streaming the file.

How do I generate download URLs for private files stored on S3?

Use Storage::temporaryUrl($path, now()->addMinutes(15)) to generate a signed URL that expires. This works with the s3 disk driver and creates a pre-signed URL that grants temporary access without making the file public. The URL is unguessable and time-limited, making it safe to send to authenticated users.

Free security check

Is your Laravel app exposed right now?

34% of Laravel apps we scan have at least one critical issue. Most teams don't find out until something breaks. Our free scan checks your live application in under 60 seconds.

18% have debug mode on
72% missing security headers
12% have exposed .env
Scan My App Free No signup required. Results in 60 seconds.