Loading
Security: protect and validate the editor image upload route
Closes #34 (closed)
Vulnerability
BladeComponentsServiceProvider::register() registered POST /bbs-image-upload with no middleware at all, and EditorImageUploadController did no authorization and no validation. As a result:
- Any anonymous visitor could write arbitrary files of any size and type (including HTML/SVG) to the public
imagesdisk and get a public URL back. This allows free hosting of phishing or spam content on client domains. - The route was outside the
webgroup, so CSRF was never checked, even though the CK editor already sends?_token=. - The route was registered on every site, including sites that never use the editor. There was no rate limit.
Fix
- Route registration moved from
register()toboot(). It now respectsroutesAreCached()and can be disabled witheditor_image_upload_enabled. - Configurable middleware, default
['web', 'auth'].webprovides the session (soauthworks for logged-in admins) and turns on CSRF checks. The CK editor already sends_token, so the component does not need to change. - Optional ability check in the new
Http\Middleware\AuthorizeEditorImageUpload: if the configured ability (defaultedit-text) is defined viaGate::define(), users without it get403. If the project never defines it,authalone applies, so nobody is locked out. - Rate limit: named limiter
bbs-editor-image-upload, 30 uploads per minute per user (falls back to IP). Can be configured or turned off. - Controller validation:
required|file|image|mimes:jpg,jpeg,png,gif,webp|max:8192. SVG is excluded explicitly throughmimes, so it is also rejected on Laravel versions whereimagestill allows SVG. On failure the controller returns422with{uploaded: 0, error: {message}}, the error format CKEditor's upload adapter expects, instead of a redirect. The filename is still generated bystore()(a random hash, with the extension taken from the file content). The storage path can now be set witheditor_image_upload_path. - README: new "Editor Image Upload" section.
- Tests: new
tests/EditorImageUploadTest.php.
New config keys (all flat, next to the existing editor_image_* keys)
| Key | Default |
|---|---|
editor_image_upload_enabled |
true |
editor_image_upload_middleware |
['web', 'auth'] |
editor_image_upload_ability |
'edit-text' |
editor_image_upload_rate_limit |
30 (per minute, null = off) |
editor_image_upload_max_size |
8192 (KB) |
editor_image_upload_mimes |
['jpg', 'jpeg', 'png', 'gif', 'webp'] |
editor_image_upload_path |
null |
The new keys are top-level, so mergeConfigFrom() supplies their defaults even in projects that published an older config file. Existing keys are unchanged.
Acceptance criteria
- Anonymous request rejected (redirect to login, or
401for JSON), tested - Authenticated user without a defined ability gets
403, tested - User with the ability can upload, same response format as before, tested
- Project without the ability keeps working with plain
auth, tested - Non-image, SVG and oversized uploads rejected, tested
- Rate limiter added, tested
- Tests have not been run yet, see below
Upgrade notes for consumers (behaviour change)
This changes the default behaviour. It ships as a patch release because it is a security fix, but it does change what consumers see:
- Uploads now require a logged-in user (
web+auth). Projects where guests are meant to upload editor images (unlikely) must seteditor_image_upload_middlewareto something like['web']. - If a project defines a Gate ability
edit-text, only users with that ability can upload. - Projects using
spatie/laravel-permissionor otherGate::before()-based permissions:Gate::has()does not see these, so onlyauthis enforced. To require the permission, set'editor_image_upload_middleware' => ['web', 'auth', 'can:edit-text']. - CSRF is now checked. The shipped CK component already sends the token. Custom upload clients must send
_tokenorX-CSRF-TOKEN. - Only jpg/jpeg/png/gif/webp up to 8 MB are accepted. SVG uploads are rejected.
- Projects that already register their own protected route for the same URI (e.g. magnus-kleine-tebbe) are unaffected, because their later registration still wins.
- If you set
editor_image_upload_enabledtofalse, the CK component still callsroute(editor_image_upload_route_name), so you must register your own route with that name. - Rollout: live sites stay exposed until each project runs
composer update berlin-bird-studios/blade-componentsand deploys (see #34 (closed), and keutzer-boos-website#4).
Not verified
The test suite was not run for this MR. The changes were written through the GitLab API without a local checkout. Please run vendor/bin/phpunit (or CI) before merging. Points to watch:
test_anonymous_request_is_redirected_to_loginrelies on Laravel 11+ falling back to/loginwhen nologinroute exists.- The upload tests use
UploadedFile::fake()->create()with explicit MIME types, so GD is not needed.