From 73c6bf4f8deea1eaf417b3ac929c0e817a203128 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 21 Nov 2025 12:09:25 +0000 Subject: [PATCH 1/3] Images: Updated access to consider public secure_restricted Had prevented public access for images when secure_restricted images was enabled (and for just secure images) when app settings allowed public access. This considers the app public setting, and adds tests to cover extra scenarios to prevent regression. --- app/Uploads/ImageService.php | 15 ++++++++-- app/Uploads/ImageStorage.php | 2 +- tests/Uploads/ImageTest.php | 54 ++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/app/Uploads/ImageService.php b/app/Uploads/ImageService.php index a26a04ac5..ed640913e 100644 --- a/app/Uploads/ImageService.php +++ b/app/Uploads/ImageService.php @@ -264,7 +264,7 @@ class ImageService return false; } - if ($this->storage->usingSecureImages() && user()->isGuest()) { + if ($this->blockedBySecureImages()) { return false; } @@ -280,13 +280,24 @@ class ImageService return false; } - if ($this->storage->usingSecureImages() && user()->isGuest()) { + if ($this->blockedBySecureImages()) { return false; } return $this->imageFileExists($image->path, $image->type); } + /** + * Check if the current user should be blocked from accessing images based on if secure images are enabled + * and if public access is enabled for the application. + */ + protected function blockedBySecureImages(): bool + { + $enforced = $this->storage->usingSecureImages() && !setting('app-public'); + + return $enforced && user()->isGuest(); + } + /** * Check if the given image path exists for the given image type and that it is likely an image file. */ diff --git a/app/Uploads/ImageStorage.php b/app/Uploads/ImageStorage.php index 38a22e3b4..abf2b429b 100644 --- a/app/Uploads/ImageStorage.php +++ b/app/Uploads/ImageStorage.php @@ -74,7 +74,7 @@ class ImageStorage return 'local'; } - // Rename local_secure options to get our image specific storage driver which + // Rename local_secure options to get our image-specific storage driver, which // is scoped to the relevant image directories. if ($localSecureInUse) { return 'local_secure_images'; diff --git a/tests/Uploads/ImageTest.php b/tests/Uploads/ImageTest.php index c5a5eb2ba..f36be8702 100644 --- a/tests/Uploads/ImageTest.php +++ b/tests/Uploads/ImageTest.php @@ -5,6 +5,8 @@ namespace Tests\Uploads; use BookStack\Entities\Repos\PageRepo; use BookStack\Uploads\Image; use BookStack\Uploads\ImageService; +use BookStack\Uploads\UserAvatars; +use BookStack\Users\Models\Role; use Illuminate\Support\Str; use Tests\TestCase; @@ -467,6 +469,26 @@ class ImageTest extends TestCase } } + public function test_avatar_images_visible_only_when_public_access_enabled_with_local_secure_restricted() + { + config()->set('filesystems.images', 'local_secure_restricted'); + $user = $this->users->admin(); + $avatars = $this->app->make(UserAvatars::class); + $avatars->assignToUserFromExistingData($user, $this->files->pngImageData(), 'png'); + + $avatarUrl = $user->getAvatar(); + + $resp = $this->get($avatarUrl); + $resp->assertRedirect('/login'); + + $this->permissions->makeAppPublic(); + + $resp = $this->get($avatarUrl); + $resp->assertOk(); + + $this->files->deleteAtRelativePath($user->avatar->path); + } + public function test_secure_restricted_images_inaccessible_without_relation_permission() { config()->set('filesystems.images', 'local_secure_restricted'); @@ -491,6 +513,38 @@ class ImageTest extends TestCase } } + public function test_secure_restricted_images_accessible_with_public_guest_access() + { + config()->set('filesystems.images', 'local_secure_restricted'); + $this->permissions->makeAppPublic(); + + $this->asEditor(); + $page = $this->entities->page(); + $this->files->uploadGalleryImageToPage($this, $page); + $image = Image::query()->where('type', '=', 'gallery') + ->where('uploaded_to', '=', $page->id) + ->first(); + + $expectedUrl = url($image->path); + $expectedPath = storage_path($image->path); + auth()->logout(); + + $this->get($expectedUrl)->assertOk(); + + $this->permissions->setEntityPermissions($page, [], []); + + $resp = $this->get($expectedUrl); + $resp->assertNotFound(); + + $this->permissions->setEntityPermissions($page, ['view'], [Role::getSystemRole('public')]); + + $this->get($expectedUrl)->assertOk(); + + if (file_exists($expectedPath)) { + unlink($expectedPath); + } + } + public function test_thumbnail_path_handled_by_secure_restricted_images() { config()->set('filesystems.images', 'local_secure_restricted'); From 9934f85ba93a9450b2a7a02559e658c5181aca61 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 21 Nov 2025 13:42:50 +0000 Subject: [PATCH 2/3] Deps: Updated PHP packages via composer --- composer.lock | 54 +++++++++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/composer.lock b/composer.lock index 282a6b239..881521a43 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.360.0", + "version": "3.362.1", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "a21055795be59f3d7c5ca6e4d52a80930dcf8c20" + "reference": "f29a49b74d5ee771f13432e16d58651de91f7e79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a21055795be59f3d7c5ca6e4d52a80930dcf8c20", - "reference": "a21055795be59f3d7c5ca6e4d52a80930dcf8c20", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/f29a49b74d5ee771f13432e16d58651de91f7e79", + "reference": "f29a49b74d5ee771f13432e16d58651de91f7e79", "shasum": "" }, "require": { @@ -153,22 +153,22 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.360.0" + "source": "https://github.com/aws/aws-sdk-php/tree/3.362.1" }, - "time": "2025-11-17T19:46:19+00:00" + "time": "2025-11-20T19:10:40+00:00" }, { "name": "bacon/bacon-qr-code", - "version": "v3.0.2", + "version": "v3.0.3", "source": { "type": "git", "url": "https://github.com/Bacon/BaconQrCode.git", - "reference": "fe259c55425b8178f77fb6d1f84ba2473e21ed55" + "reference": "36a1cb2b81493fa5b82e50bf8068bf84d1542563" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/fe259c55425b8178f77fb6d1f84ba2473e21ed55", - "reference": "fe259c55425b8178f77fb6d1f84ba2473e21ed55", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/36a1cb2b81493fa5b82e50bf8068bf84d1542563", + "reference": "36a1cb2b81493fa5b82e50bf8068bf84d1542563", "shasum": "" }, "require": { @@ -208,9 +208,9 @@ "homepage": "https://github.com/Bacon/BaconQrCode", "support": { "issues": "https://github.com/Bacon/BaconQrCode/issues", - "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.2" + "source": "https://github.com/Bacon/BaconQrCode/tree/v3.0.3" }, - "time": "2025-11-16T22:59:48+00:00" + "time": "2025-11-19T17:15:36+00:00" }, { "name": "brick/math", @@ -3613,31 +3613,31 @@ }, { "name": "nunomaduro/termwind", - "version": "v2.3.2", + "version": "v2.3.3", "source": { "type": "git", "url": "https://github.com/nunomaduro/termwind.git", - "reference": "eb61920a53057a7debd718a5b89c2178032b52c0" + "reference": "6fb2a640ff502caace8e05fd7be3b503a7e1c017" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/eb61920a53057a7debd718a5b89c2178032b52c0", - "reference": "eb61920a53057a7debd718a5b89c2178032b52c0", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/6fb2a640ff502caace8e05fd7be3b503a7e1c017", + "reference": "6fb2a640ff502caace8e05fd7be3b503a7e1c017", "shasum": "" }, "require": { "ext-mbstring": "*", "php": "^8.2", - "symfony/console": "^7.3.4" + "symfony/console": "^7.3.6" }, "require-dev": { "illuminate/console": "^11.46.1", "laravel/pint": "^1.25.1", "mockery/mockery": "^1.6.12", - "pestphp/pest": "^2.36.0 || ^3.8.4", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.1.3", "phpstan/phpstan": "^1.12.32", "phpstan/phpstan-strict-rules": "^1.6.2", - "symfony/var-dumper": "^7.3.4", + "symfony/var-dumper": "^7.3.5", "thecodingmachine/phpstan-strict-rules": "^1.0.0" }, "type": "library", @@ -3680,7 +3680,7 @@ ], "support": { "issues": "https://github.com/nunomaduro/termwind/issues", - "source": "https://github.com/nunomaduro/termwind/tree/v2.3.2" + "source": "https://github.com/nunomaduro/termwind/tree/v2.3.3" }, "funding": [ { @@ -3696,7 +3696,7 @@ "type": "github" } ], - "time": "2025-10-18T11:10:27+00:00" + "time": "2025-11-20T02:34:59+00:00" }, { "name": "onelogin/php-saml", @@ -8587,16 +8587,16 @@ }, { "name": "nunomaduro/collision", - "version": "v8.8.2", + "version": "v8.8.3", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb" + "reference": "1dc9e88d105699d0fee8bb18890f41b274f6b4c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", - "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/1dc9e88d105699d0fee8bb18890f41b274f6b4c4", + "reference": "1dc9e88d105699d0fee8bb18890f41b274f6b4c4", "shasum": "" }, "require": { @@ -8618,7 +8618,7 @@ "laravel/sanctum": "^4.1.1", "laravel/tinker": "^2.10.1", "orchestra/testbench-core": "^9.12.0 || ^10.4", - "pestphp/pest": "^3.8.2", + "pestphp/pest": "^3.8.2 || ^4.0.0", "sebastian/environment": "^7.2.1 || ^8.0" }, "type": "library", @@ -8682,7 +8682,7 @@ "type": "patreon" } ], - "time": "2025-06-25T02:12:12+00:00" + "time": "2025-11-20T02:55:25+00:00" }, { "name": "phar-io/manifest", From 22a7772c3d62dba0c80abff731ef815d26bf9e01 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 21 Nov 2025 13:57:38 +0000 Subject: [PATCH 3/3] Env: Added storage type to default example env Provides greater consideration to the storage type used and the fact that it'll place images in public space by default. --- .env.example | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.env.example b/.env.example index 4dee3b334..13114b8b0 100644 --- a/.env.example +++ b/.env.example @@ -26,6 +26,13 @@ DB_DATABASE=database_database DB_USERNAME=database_username DB_PASSWORD=database_user_password +# Storage system to use +# By default files are stored on the local filesystem, with images being placed in +# public web space so they can be efficiently served directly by the web-server. +# For other options with different security levels & considerations, refer to: +# https://www.bookstackapp.com/docs/admin/upload-config/ +STORAGE_TYPE=local + # Mail system to use # Can be 'smtp' or 'sendmail' MAIL_DRIVER=smtp