diff --git a/app/Uploads/ImageService.php b/app/Uploads/ImageService.php index a26a04ac5c6..ed640913ec3 100644 --- a/app/Uploads/ImageService.php +++ b/app/Uploads/ImageService.php @@ -264,7 +264,7 @@ public function pathAccessible(string $imagePath): bool return false; } - if ($this->storage->usingSecureImages() && user()->isGuest()) { + if ($this->blockedBySecureImages()) { return false; } @@ -280,13 +280,24 @@ public function imageAccessible(Image $image): bool 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 38a22e3b498..abf2b429b1c 100644 --- a/app/Uploads/ImageStorage.php +++ b/app/Uploads/ImageStorage.php @@ -74,7 +74,7 @@ protected function getDiskName(string $imageType): string 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/composer.lock b/composer.lock index 282a6b23988..881521a43d8 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", diff --git a/tests/Uploads/ImageTest.php b/tests/Uploads/ImageTest.php index c5a5eb2ba0f..f36be87023d 100644 --- a/tests/Uploads/ImageTest.php +++ b/tests/Uploads/ImageTest.php @@ -5,6 +5,8 @@ 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 @@ public function test_system_images_remain_public_with_local_secure_restricted() } } + 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 @@ public function test_secure_restricted_images_inaccessible_without_relation_perm } } + 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');