Currently Available: Need a skilled Software Developer for your next project?
Categories
Laravel

How to Store and Serve Private Files in Laravel

When a Laravel site handles invoices, identity documents, or other files that only specific users should see, store each file outside the web-accessible directory and check the requester’s permission before returning it. A file saved under Laravel’s public disk can be fetched by anyone who knows its URL. A file saved on a private disk stays off the public web path until Laravel authorizes a request and serves it.

A disk is Laravel’s name for a place to store files, such as a server directory or an S3 bucket. Use a private disk to save uploads. Store each file’s path in a database record, then serve it through a route that requires sign-in and checks whether the requester has access.

Store files on a private disk

Laravel’s public disk stores files anyone can access. The storage:link command creates a link from that disk to a directory under the web root, so people can open its files by URL. Keep sensitive uploads off the public disk. Laravel’s storage directory is generally outside the web root, so code can serve files from there after checking access. See the explanation of public and private Laravel storage.

Define a private disk in config/filesystems.php. This keeps your code from relying on the default local-disk path, which can vary by Laravel version:

'disks' => [

    // Other disks...

    'private' => [
        'driver' => 'local',
        'root' => storage_path('app/private'),
        'visibility' => 'private',
        'throw' => false,
    ],
],

Keep the private directory outside the web server’s document root. Do not include it among the symbolic links created by storage:link. The web server’s document root should point to Laravel’s public directory.

Save an upload with Laravel’s storage API:

$path = $request->file('file')->store('documents', 'private');

Laravel generates a storage name for the upload and returns a relative path, such as documents/.... Save that path in a database record with the file owner or other access information. Do not build storage paths from a filename supplied by the client. Predictable names are easier to guess, and a user-supplied path must not determine which file Laravel reads.

Check uploads before storing them. Laravel’s mimes rule checks the file type, and the max rule sets a size limit in kilobytes. Choose which file types and sizes your service accepts. For sensitive uploads, scan files for malware before making them available.

Authorize each download

Authentication confirms who is making a request. Authorization checks whether that person can access a specific file. Protect the download route with authentication middleware, then check the file’s owner or access rules before returning it.

For example, a controller can authorize access to a database-backed document and use Storage::download to send the file:

use App\Models\Document;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;

class DownloadDocumentController
{
    public function __invoke(Document $document)
    {
        Gate::authorize('view', $document);

        return Storage::disk('private')->download(
            $document->path,
            basename($document->original_name)
        );
    }
}

Define the view policy to apply your access rules, such as checking whether the signed-in user owns the document. Add your authentication middleware to the route:

Route::get('/documents/{document}/download', DownloadDocumentController::class)
    ->middleware('auth');

basename strips directory components from the display filename. After Laravel loads the document record and approves the request, pass the database path to the storage method. Never take a file path directly from a query parameter or request body. Laravel’s Storage facade saves and retrieves files through named disks, as shown in this storage facade example.

For API routes, use the authentication middleware and guard configured for your API. Older examples that rely on auth:api may not match your current Laravel setup.

Use temporary URLs for private cloud files

If you store files in S3, keep the bucket and objects private. Laravel can generate a temporary URL that grants access until its expiration time:

$url = Storage::disk('s3')->temporaryUrl(
    $document->path,
    $expiresAt
);

Set $expiresAt according to the access period your service needs. A temporary URL is a bearer link: anyone who obtains it can use it until it expires, even if that person is not signed in to your site. Authorize the user before generating the link, and configure the bucket so it does not expose the same object through a public URL. Laravel storage guidance also describes temporary file URLs.

Temporary URL support depends on the storage driver and its configuration. Verify that your chosen disk can generate these URLs before relying on them. For local files, an authenticated Laravel route gives you a place to check authorization on each download request.

Keep upload and storage rules aligned

The private directory blocks direct web access to files, but it does not decide who can download them. The route and authorization policy make that decision. A private S3 bucket also exposes files if its visibility setting or bucket policy grants public access. Check the disk and bucket settings, then test whether someone can access a file without signing in.

Use Laravel’s file validation rules to limit file types and sizes. A file’s original name or extension does not prove what it contains. If you scan files for malware, keep them unavailable to users until the scan finishes. These measures address common upload risks described in this Laravel security overview.

What I'm building

Delegate tasks. Get software.

Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.

Take a look at vroni.com

Email updates

Usually a new article and a few links I found interesting.

No spam. Unsubscribe with one click.

Leave a Reply

Your email address will not be published. Required fields are marked *