Compare commits

...

5 Commits

Author SHA1 Message Date
Dan Brown
ad8fc95521 Updated version and assets for release v25.11.3 2025-11-21 14:02:09 +00:00
Dan Brown
cca066a258 Merge branch 'development' into release 2025-11-21 14:01:06 +00:00
Dan Brown
22a7772c3d 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.
2025-11-21 13:57:38 +00:00
Dan Brown
9934f85ba9 Deps: Updated PHP packages via composer 2025-11-21 13:42:50 +00:00
Dan Brown
73c6bf4f8d 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.
2025-11-21 12:09:25 +00:00
7 changed files with 104 additions and 32 deletions

View File

@@ -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

View File

@@ -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.
*/

View File

@@ -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';

54
composer.lock generated
View File

@@ -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",

View File

@@ -1 +1 @@
a75aa1a640d312e5e6a52f63b121daf5bca1e4ad11aaf9a162c8f91e8e2e00ed
54cd39cc8e939ff56c7785b653950e7f9c198902779a2babf03d279ec07edc2c

View File

@@ -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');

View File

@@ -1 +1 @@
v25.11.2
v25.11.3