fix: require authentication and mail permissions on preview and attachment routes - #84
Closed
Baspa wants to merge 4 commits into
Closed
fix: require authentication and mail permissions on preview and attachment routes#84Baspa wants to merge 4 commits into
Baspa wants to merge 4 commits into
Conversation
Contributor
Author
|
Superseded by backstagephp/cms#299.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #83
Problem
The stored mail preview and attachment download routes were reachable without authentication, and the attachment route never verified that the requested attachment belonged to the mail in the URL.
Anyone able to reach the panel path could read the full HTML body of any logged mail and download any stored attachment by supplying a numeric ID.
Why authentication was missing
Mails::routes()registered two bare routes with no middleware. Filament invokes$panel->getRoutes()inpackages/panels/routes/web.php:30, inside the$panel->getMiddleware()group — which is onlypanel:{id}plus the panel's web middleware. The$panel->getAuthMiddleware()group does not open until line 58.->tenantRoutes()has the same problem: it is invoked at line 191, in a group that appliesgetTenantMiddleware()but sits outside the auth group as well.Both of these were what the README documented, so installs following the README were exposed. Only
->authenticatedRoutes()and->authenticatedTenantRoutes()sit inside the auth group.Why authorization was missing
MailsPlugin::userCanManageMails()was wired into every Resource, Page and Widget, but neither controller consulted it.Why any attachment resolved under any mail
MailDownloadControllerdestructured{mail}out of the URL and then never used it:Fix
Access control is attached inside
Mails::routes()itself, rather than left to the consumer'sPanelProvider:This was deliberate. A documentation-only fix would leave every existing install vulnerable until they edited their own
PanelProvider. Attaching the middleware where the routes are defined means the protection travels with them, whichever registration method the consumer picked.Reusing Filament's own
Authenticategives us the panel's configured guard, the redirect to the panel login URL, and theFilamentUser::canAccessPanel()check, with behaviour identical to every other page in the panel.The new
EnsureUserCanManageMailsmiddleware guards on$panel->hasPlugin('mails')before callingMailsPlugin::get()— that call resolves throughfilament('mails')and throws when the plugin is absent from the current panel, which would otherwise be a 500 rather than a 403.Attachments are now scoped through their mail:
Mail::attachments()is aHasMany, so an unrelated attachment ID fails the query and raises a 404 — the file is never read. This also removes the...$argumentssplat and itscount($arguments) === 4tenant branch; named route parameters resolve identically with and without a{tenant}segment, and the variadic signature is what made it easy to silently drop{mail}in the first place.The
{filename}segment stays unused on purpose:downloadFileFromStorage()builds the storage path from$this->filenameon the model, so the URL segment never reaches the filesystem.Preview hardening:
find()→findOrFail()(an unknown ID was previously a 500 fromnull->html), the model is resolved fromconfig('mails.models.mail')like the download controller, and the response now sendsX-Content-Type-Options: nosniffandContent-Security-Policy: frame-ancestors 'self'. The body is still served verbatim — the preview iframe needs it, and it is same-origin soframe-ancestors 'self'permits it.Behaviour
canAccessPanel()falsecanManageMails()falseTests
The existing suite was unit-style with no database or authentication, so this adds the scaffolding for it:
TestCasenow registersBackstage\Mails\Laravel\MailsServiceProvider— Testbench does not load it automatically, and without itconfig('mails.models.mail')isnull.defineDatabaseMigrations()creates auserstable and runs the laravel-mails migration stubs, which ship as.php.stuband are never auto-loaded.tests/Fixtures/User.phpis an authenticatableFilamentUser.tests/MailRouteSecurityTest.phpcovers every row of the table above. Note that the test panel registers the routes with->routes()— the vulnerable method — on purpose, so the tests prove the middleware protects that path rather than only provingauthenticatedRoutes()works.13 tests, 25 assertions, all passing.
Docs
README now documents
authenticatedRoutes()/authenticatedTenantRoutes(), with a note that the routes enforce the permission themselves regardless.Not addressed here
Mailmodel has no tenant relation to scope by;{tenant}is only a URL segment. WithauthenticatedTenantRoutes()your own tenant middleware handles isolation. Worth a separate issue if cross-tenant access matters.Unrelated issues noticed
composer analysecannot run —phpstanis not inrequire-dev, andphpstan.neon.distusescheckOctaneCompatibility/checkModelProperties, which were removed in PHPStan 2 / Larastan 3. The PHPStan workflow is therefore broken independently of this PR. Verified locally against an equivalent ad-hoc config: this branch introduces no new errors and removes two.MailAttachmentFactoryinbackstage/laravel-mailsis broken — itsdefinition()returnstype/ip/hostname/payload, none of which are columns onmail_attachments. The tests create attachments through the relation instead.Reported by Ahmet Akdas (Digital Impact).