Images: Added content checking and app range support to streaming

Mainly to prevent non-image files being streamed in scenarios where the
underlying image data is somehow a non-image.

Thanks to Emanuele Cervelli (https://github.com/M4nu02) for reporting.
This commit is contained in:
Dan Brown
2026-08-19 17:52:35 +01:00
parent fbe83a1895
commit ac0348a79f
3 changed files with 38 additions and 1 deletions
+14 -1
View File
@@ -4,6 +4,8 @@ namespace BookStack\Uploads;
use BookStack\Entities\Queries\EntityQueries;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\PrettyException;
use BookStack\Http\DownloadResponseFactory;
use Exception;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@@ -368,7 +370,18 @@ class ImageService
{
$disk = $this->storage->getDisk($imageType);
return $disk->response($path);
$stream = $disk->stream($path);
$fileSize = $disk->size($path);
$imageName = basename($path);
$downloadResponseFactory = new DownloadResponseFactory(request());
$response = $downloadResponseFactory->streamedInline($stream, $imageName, $fileSize);
$contentType = $response->headers->get('Content-Type');
if (!str_starts_with($contentType, 'image/')) {
throw new PrettyException('Invalid non-image file type when streaming from storage', 415);
}
return $response;
}
/**
+8
View File
@@ -68,6 +68,14 @@ class ImageStorageDisk
return $this->filesystem->readStream($this->adjustPathForDisk($path));
}
/**
* Get the size of the file at the given path.
*/
public function size(string $path): int
{
return $this->filesystem->size($this->adjustPathForDisk($path));
}
/**
* Save the given image data at the given path. Can choose to set
* the image as public which will update its visibility after saving.
+16
View File
@@ -333,6 +333,22 @@ class ImageGalleryApiTest extends TestCase
$resp->assertStatus(404);
}
public function test_read_data_endpoint_does_not_serve_non_image_files()
{
$this->actingAsApiAdmin();
$imagePage = $this->entities->page();
$data = $this->files->uploadGalleryImageToPage($this, $imagePage, 'test-image.png');
$image = Image::findOrFail($data['response']->id);
$storagePath = public_path($image->path);
file_put_contents($storagePath, '<html><body><p>This is totally not an image file!</p></body></html>');
$resp = $this->get("{$this->baseEndpoint}/{$image->id}/data");
$resp->assertStatus(415);
$resp->assertJson($this->errorResponse('Invalid non-image file type when streaming from storage.', 415));
}
public function test_read_url_data_endpoint()
{
$this->actingAsApiAdmin();