Skip to content

fix: Use signed URLs for attachments on private disks (#126) - #127

Merged
luisdalmolin merged 2 commits into
mainfrom
fix/private-disk-attachment-urls
Oct 19, 2026
Merged

luisdalmolin merged 2 commits into
mainfrom
fix/private-disk-attachment-urls

Conversation

@luisdalmolin

Copy link
Copy Markdown
Member

Fixes #126

Problem

CommentAttachment::getUrl() always called Storage::disk($disk)->url($path), which returns an unsigned URL. Laravel 11+ serves the private local disk ('serve' => true) through its /storage/{path} route, which aborts unless the request has a valid signature or the disk's visibility is public. So with COMMENTIONS_ATTACHMENTS_DISK=local, every attachment link returned 403, or 404 in production. A private S3 bucket fails the same way.

Fix

  • Any disk not configured with 'visibility' => 'public' now gets temporaryUrl(). A missing visibility counts as private, the same rule Laravel's own file serving uses.
  • The expiry is a new attachments.url_expiration config option, in minutes (COMMENTIONS_ATTACHMENTS_URL_EXPIRATION, default 30).
  • Disks that can't sign URLs (providesTemporaryUrls() is false, e.g. a local disk without serve) still get the plain url(), so nothing throws.
  • README: documents the option and adds a "Storing attachments privately" section.

Behavior change

An S3 disk with no visibility key now gets signed URLs. If that bucket is actually public through a bucket policy, the links still work, but they now expire. Setting 'visibility' => 'public' on the disk brings back plain URLs.

Not in scope

A signed URL isn't tied to a user, so anyone holding the link can open the file until it expires. Checking per user whether someone can see an attachment would need an authorized download route, which can come as a follow-up.

Tests

  • A private local disk (real, not faked) produces a URL that the framework's storage route serves with 200. Before the fix this returned 403, the same as the issue.
  • The expires timestamp follows url_expiration.
  • A public disk keeps the plain URL.
  • A disk that can't sign URLs falls back to the plain URL.

./vendor/bin/pest: 186 passed. composer lint:check passes. composer static-analysis reports the same 7 errors as main, all in tests/Livewire/CommentReactionTest.php.

getUrl() always called Storage::url(), which returns an unsigned URL.
On a private disk such as Laravel 11+'s `local` disk, the framework's
storage route rejects unsigned requests, so every attachment link
returned 403 (404 in production).

Disks not configured with `'visibility' => 'public'` now get a
temporaryUrl() that expires after `attachments.url_expiration` minutes
(default 30). Disks that cannot sign URLs keep the plain URL.

Fixes #126
testbench 9 (Laravel 11) ships a local disk without 'serve' => true, so
the signed storage route was never registered and the private-disk
attachment tests failed. Enable it to match the Laravel 11+ skeleton.
@luisdalmolin
luisdalmolin merged commit 5f5e4ba into main Oct 19, 2026
20 checks passed
@luisdalmolin
luisdalmolin deleted the fix/private-disk-attachment-urls branch October 19, 2026 10:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Storing attachments privately

1 participant