mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-10-12 02:01:37 +00:00
- Updates ZIP imports to validate that actual mime type of book cover images to ensure they are a supported image file. - Updates lower level ImageStorage to validate extensions of images when cleaning their name pre-storage. - Updates ZIP import Image and Book handling so book cover and image files have their extension forced from file mime types in the same way. Added testing to cover. Thanks to the submersion.ai security research team for reporting.
162 lines
5.1 KiB
PHP
162 lines
5.1 KiB
PHP
<?php
|
|
|
|
namespace BookStack\Uploads;
|
|
|
|
use BookStack\Exceptions\ImageUploadException;
|
|
use Illuminate\Filesystem\FilesystemManager;
|
|
use Illuminate\Support\Str;
|
|
|
|
class ImageStorage
|
|
{
|
|
public function __construct(
|
|
protected FilesystemManager $fileSystem,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Get the storage disk for the given image type.
|
|
*/
|
|
public function getDisk(string $imageType = ''): ImageStorageDisk
|
|
{
|
|
$diskName = $this->getDiskName($imageType);
|
|
|
|
return new ImageStorageDisk(
|
|
$diskName,
|
|
$this->fileSystem->disk($diskName),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if "local secure restricted" (Fetched behind auth, with permissions enforced)
|
|
* is currently active in the instance.
|
|
*/
|
|
public function usingSecureRestrictedImages(): bool
|
|
{
|
|
return config('filesystems.images') === 'local_secure_restricted';
|
|
}
|
|
|
|
/**
|
|
* Check if "local secure" (Fetched behind auth, either with or without permissions enforced)
|
|
* is currently active in the instance.
|
|
*/
|
|
public function usingSecureImages(): bool
|
|
{
|
|
return config('filesystems.images') === 'local_secure' || $this->usingSecureRestrictedImages();
|
|
}
|
|
|
|
/**
|
|
* Clean up an image file name to be both URL and storage safe.
|
|
* @throws ImageUploadException
|
|
*/
|
|
public function cleanImageFileName(string $name): string
|
|
{
|
|
$name = str_replace(' ', '-', $name);
|
|
$nameParts = explode('.', $name);
|
|
$extension = array_pop($nameParts);
|
|
$name = implode('-', $nameParts);
|
|
$name = Str::slug($name);
|
|
|
|
if (!ImageService::isExtensionSupported($extension)) {
|
|
throw new ImageUploadException('Non supported image extension used when storing image: ' . $extension);
|
|
}
|
|
|
|
if (strlen($name) === 0) {
|
|
$name = Str::random(10);
|
|
}
|
|
|
|
return $name . '.' . $extension;
|
|
}
|
|
|
|
/**
|
|
* Get the name of the storage disk to use.
|
|
*/
|
|
protected function getDiskName(string $imageType): string
|
|
{
|
|
$storageType = strtolower(config('filesystems.images'));
|
|
$localSecureInUse = ($storageType === 'local_secure' || $storageType === 'local_secure_restricted');
|
|
|
|
// Ensure system images (App logo) are uploaded to a public space
|
|
if ($imageType === 'system' && $localSecureInUse) {
|
|
return 'local';
|
|
}
|
|
|
|
// 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';
|
|
}
|
|
|
|
return $storageType;
|
|
}
|
|
|
|
/**
|
|
* Get a storage path for the given image URL.
|
|
* Ensures the path will start with "uploads/images".
|
|
* Returns null if the url cannot be resolved to a local URL.
|
|
*/
|
|
public function urlToPath(string $url): ?string
|
|
{
|
|
$url = ltrim(trim($url), '/');
|
|
|
|
// Handle potential relative paths
|
|
$isRelative = !str_starts_with($url, 'http');
|
|
if ($isRelative) {
|
|
if (str_starts_with(strtolower($url), 'uploads/images')) {
|
|
return trim($url, '/');
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Handle local images based on paths on the same domain
|
|
$potentialHostPaths = [
|
|
url('uploads/images/'),
|
|
$this->getPublicUrl('/uploads/images/'),
|
|
];
|
|
|
|
foreach ($potentialHostPaths as $potentialBasePath) {
|
|
$potentialBasePath = strtolower($potentialBasePath);
|
|
if (str_starts_with(strtolower($url), $potentialBasePath)) {
|
|
return 'uploads/images/' . trim(substr($url, strlen($potentialBasePath)), '/');
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Gets a public facing url for an image or location at the given path.
|
|
*/
|
|
public static function getPublicUrl(string $filePath): string
|
|
{
|
|
return static::getPublicBaseUrl() . '/' . ltrim($filePath, '/');
|
|
}
|
|
|
|
/**
|
|
* Get the public base URL used for images.
|
|
* Will not include any path element of the image file, just the base part
|
|
* from where the path is then expected to start from.
|
|
* If s3-style store is in use it will default to guessing a public bucket URL.
|
|
*/
|
|
protected static function getPublicBaseUrl(): string
|
|
{
|
|
$storageUrl = config('filesystems.url');
|
|
|
|
// Get the standard public s3 url if s3 is set as storage type
|
|
// Uses the nice, short URL if bucket name has no periods in otherwise the longer
|
|
// region-based url will be used to prevent http issues.
|
|
if (!$storageUrl && config('filesystems.images') === 's3') {
|
|
$storageDetails = config('filesystems.disks.s3');
|
|
if (!str_contains($storageDetails['bucket'], '.')) {
|
|
$storageUrl = 'https://' . $storageDetails['bucket'] . '.s3.amazonaws.com';
|
|
} else {
|
|
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
|
|
}
|
|
}
|
|
|
|
$basePath = $storageUrl ?: url('/');
|
|
|
|
return rtrim($basePath, '/');
|
|
}
|
|
}
|