From 67fdcf7f08c2aca8f162484ca3730e07f2ec0398 Mon Sep 17 00:00:00 2001 From: scouzinier Date: Tue, 28 Apr 2026 11:26:33 +0200 Subject: [PATCH 01/18] update thumbnail, allow user to choose output format --- public/theme/skeleton/partials/_image.twig | 2 +- src/Controller/ImageController.php | 35 ++++++++++++++++++---- src/Twig/ImageExtension.php | 4 +-- src/Utils/ThumbnailHelper.php | 6 ++-- 4 files changed, 36 insertions(+), 11 deletions(-) diff --git a/public/theme/skeleton/partials/_image.twig b/public/theme/skeleton/partials/_image.twig index d697419b6..18205ecfd 100644 --- a/public/theme/skeleton/partials/_image.twig +++ b/public/theme/skeleton/partials/_image.twig @@ -1,7 +1,7 @@ {% if image %}
- {{ (record|image).alt|default(record|title) }} + {{ (record|image).alt|default(record|title) }} {% if image.alt|default() %}
{{ image.alt }}
diff --git a/src/Controller/ImageController.php b/src/Controller/ImageController.php index 3b3de5dca..94516dcb1 100644 --- a/src/Controller/ImageController.php +++ b/src/Controller/ImageController.php @@ -20,6 +20,8 @@ class ImageController { + private const SUPPORTED_FORMATS = ['jpg', 'webp', 'png', 'gif', 'avif']; + private Server $server; /** @@ -45,17 +47,20 @@ public function thumbnail(Request $request, string $paramString, string $filenam return $this->sendErrorImage(); } + $this->parseParameters($paramString); + $urlFilename = $filename; + $sourceFilename = $this->parseFormatFromFilename($filename); + try { - $filename = PathCanonicalize::canonicalize($this->getPath($request), $filename, true); + $sourceFilename = PathCanonicalize::canonicalize($this->getPath($request), $sourceFilename, true); } catch (Exception) { return $this->sendErrorImage(); } - $this->parseParameters($paramString); $this->createServer($request); - $this->saveThumb($request, $filename); + $this->saveThumb($request, $sourceFilename, $urlFilename); - return $this->buildResponse($request, $filename); + return $this->buildResponse($request, $sourceFilename); } private function createServer(Request $request): void @@ -82,7 +87,19 @@ private function getPath(Request $request, ?string $path = null, bool $absolute return $this->config->getPath($path, $absolute, $additional); } - private function saveThumb(Request $request, string $filename): void + private function parseFormatFromFilename(string $filename): string + { + $ext = mb_strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + if ($this->isSupportedFormat($ext) && pathinfo(pathinfo($filename, PATHINFO_FILENAME), PATHINFO_EXTENSION) !== '') { + $this->parameters['fm'] = $ext; + + return substr($filename, 0, -(mb_strlen($ext) + 1)); + } + + return $filename; + } + + private function saveThumb(Request $request, string $filename, string $urlFilename = ''): void { if (! $this->config->get('general/thumbnails/save_files', true)) { return; @@ -95,7 +112,7 @@ private function saveThumb(Request $request, string $filename): void $thumbPath = Path::join( $this->getPath($request, 'thumbs'), $this->parameterPath(), - $filename + $urlFilename ?: $filename ); try { @@ -103,6 +120,7 @@ private function saveThumb(Request $request, string $filename): void $filesystem->mkdir(dirname($thumbPath), $folderMode); $filesystem->dumpFile($thumbPath, $imageBlob); $filesystem->chmod($thumbPath, $fileMode); + } catch (Throwable) { // Fail silently, output user-friendly exception elsewhere. } @@ -203,6 +221,11 @@ private function testFit(string $fit): bool return (bool) preg_match('/^(contain|max|fill|stretch|crop)(-.+)?/', $fit); } + private function isSupportedFormat(string $format): bool + { + return in_array($format, self::SUPPORTED_FORMATS, true); + } + public function parseFit(string $fit): string { return match ($fit) { diff --git a/src/Twig/ImageExtension.php b/src/Twig/ImageExtension.php index c41e42ac2..889c71e5d 100644 --- a/src/Twig/ImageExtension.php +++ b/src/Twig/ImageExtension.php @@ -102,11 +102,11 @@ public function showImage($image, ?int $width = null, ?int $height = null, ?bool /** * @param ImageField|array|string $image */ - public function thumbnail($image, ?int $width = null, ?int $height = null, ?string $location = null, ?string $path = null, ?string $fit = null, ?int $quality = null): string + public function thumbnail($image, ?int $width = null, ?int $height = null, ?string $location = null, ?string $path = null, ?string $fit = null, ?int $quality = null, ?string $format = null): string { $filename = $this->getFilename($image, true); - return $this->thumbnailHelper->path($filename, $width, $height, $location, $path, $fit, $quality); + return $this->thumbnailHelper->path($filename, $width, $height, $location, $path, $fit, $quality, $format); } /** diff --git a/src/Utils/ThumbnailHelper.php b/src/Utils/ThumbnailHelper.php index ae668d0ce..fc5b04ec6 100644 --- a/src/Utils/ThumbnailHelper.php +++ b/src/Utils/ThumbnailHelper.php @@ -38,7 +38,7 @@ private function parameters(?int $width = null, ?int $height = null, ?string $fi return implode('×', array_filter([$width, $height, $quality, $fit, $location])); } - public function path(?string $filename = null, ?int $width = null, ?int $height = null, ?string $location = null, ?string $path = null, ?string $fit = null, ?int $quality = null): string + public function path(?string $filename = null, ?int $width = null, ?int $height = null, ?string $location = null, ?string $path = null, ?string $fit = null, ?int $quality = null, ?string $format = null): string { if (! $filename) { return '/assets/images/placeholder.png'; @@ -47,7 +47,9 @@ public function path(?string $filename = null, ?int $width = null, ?int $height if ($path) { $filename = $path . '/' . $filename; } - + if($format){ + $filename .= '.' . $format; + } $paramString = $this->parameters($width, $height, $fit, $location, $quality); $filename = Str::ensureStartsWith($filename, '/'); From 00d5b9fbffc77d38cbf5937ba0f3e5e42e682db3 Mon Sep 17 00:00:00 2001 From: scouzinier Date: Tue, 28 Apr 2026 11:36:38 +0200 Subject: [PATCH 02/18] typo --- src/Controller/ImageController.php | 3 +-- src/Utils/ThumbnailHelper.php | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Controller/ImageController.php b/src/Controller/ImageController.php index 94516dcb1..dbd1ee661 100644 --- a/src/Controller/ImageController.php +++ b/src/Controller/ImageController.php @@ -93,7 +93,7 @@ private function parseFormatFromFilename(string $filename): string if ($this->isSupportedFormat($ext) && pathinfo(pathinfo($filename, PATHINFO_FILENAME), PATHINFO_EXTENSION) !== '') { $this->parameters['fm'] = $ext; - return substr($filename, 0, -(mb_strlen($ext) + 1)); + return mb_substr($filename, 0, -(mb_strlen($ext) + 1)); } return $filename; @@ -120,7 +120,6 @@ private function saveThumb(Request $request, string $filename, string $urlFilena $filesystem->mkdir(dirname($thumbPath), $folderMode); $filesystem->dumpFile($thumbPath, $imageBlob); $filesystem->chmod($thumbPath, $fileMode); - } catch (Throwable) { // Fail silently, output user-friendly exception elsewhere. } diff --git a/src/Utils/ThumbnailHelper.php b/src/Utils/ThumbnailHelper.php index fc5b04ec6..93006613b 100644 --- a/src/Utils/ThumbnailHelper.php +++ b/src/Utils/ThumbnailHelper.php @@ -47,7 +47,7 @@ public function path(?string $filename = null, ?int $width = null, ?int $height if ($path) { $filename = $path . '/' . $filename; } - if($format){ + if ($format) { $filename .= '.' . $format; } $paramString = $this->parameters($width, $height, $fit, $location, $quality); From d1051477e5df83a4ed32e9b614e1c8dc6ed45d66 Mon Sep 17 00:00:00 2001 From: scouzinier Date: Mon, 4 May 2026 22:44:58 +0200 Subject: [PATCH 03/18] Update thumbnail file path in Twig and filesystem to match image URL structure, allowing Apache/Nginx to serve them directly without any PHP call --- src/Controller/ImageController.php | 59 ++++++++++++++++++++++++++---- src/Utils/ThumbnailHelper.php | 4 +- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/src/Controller/ImageController.php b/src/Controller/ImageController.php index dbd1ee661..aca0b36ed 100644 --- a/src/Controller/ImageController.php +++ b/src/Controller/ImageController.php @@ -172,7 +172,7 @@ private function buildResponse(Request $request, string $filename): Response } } - private function parseParameters(string $paramString): void + private function parseParametersold(string $paramString): void { $raw = explode('×', (string) preg_replace('/([0-9])(x)([0-9a-z])/i', '\1×\3', $paramString)); @@ -198,6 +198,50 @@ private function parseParameters(string $paramString): void } } + private function parseParameters(string $paramString): void + { + $raw = explode('×', (string) preg_replace('/([0-9])(x)([0-9a-z])/i', '\1×\3', $paramString)); + $defaultFit = $this->config->get('general/thumbnails/default_cropping', 'default'); + + $this->parameters = [ + 'w' => (isset($raw[0]) && is_numeric($raw[0])) ? (int) $raw[0] : 400, + 'h' => (isset($raw[1]) && is_numeric($raw[1])) ? (int) $raw[1] : 300, + 'fm' => '', + 'fit' => '', + 'location' => 'files', + 'q' => 80, + ]; + + $remaining = array_values(array_filter( + array_slice($raw, 2), + static fn ($value): bool => $value !== null && $value !== '' + )); + + if (isset($remaining[0]) && is_numeric($remaining[0]) && 0 <= (int) $remaining[0] && (int) $remaining[0] <= 100) { + $this->parameters['q'] = (int) array_shift($remaining); + } + + foreach ($remaining as $token) { + $token = (string) $token; + $normalizedToken = mb_strtolower($token); + + if ($this->parameters['fm'] === '' && $this->isSupportedFormat($normalizedToken)) { + $this->parameters['fm'] = $normalizedToken; + continue; + } + + $fit = $this->parseFit($normalizedToken); + if ($this->testFit($fit)) { + $this->parameters['fit'] = $fit; + continue; + } + + if ($this->parameters['location'] === 'files') { + $this->parameters['location'] = $token; + } + } + } + private function isSvg(string $filename): bool { $extension = pathinfo($filename, PATHINFO_EXTENSION); @@ -239,14 +283,15 @@ public function parseFit(string $fit): string private function parameterPath(): string { - return sprintf( - '%d_%d_%d_%s_%s', + $parts = array_filter([ $this->parameters['w'] ?? 0, $this->parameters['h'] ?? 0, - $this->parameters['q'] ?? 0, - $this->parameters['fit'] ?? '', - $this->parameters['location'] ?? '' - ); + $this->parameters['q'] ?? 80, + $this->parameters['fit'] ?? null, + $this->parameters['location'] ?? 'files', + ], fn ($v) => $v !== null && $v !== '' && $v !== 0); + + return implode('×', $parts); } public function sendErrorImage(): Response diff --git a/src/Utils/ThumbnailHelper.php b/src/Utils/ThumbnailHelper.php index 93006613b..33a17b932 100644 --- a/src/Utils/ThumbnailHelper.php +++ b/src/Utils/ThumbnailHelper.php @@ -27,8 +27,8 @@ private function parameters(?int $width = null, ?int $height = null, ?string $fi $height = 10000; } - if ($location === 'files') { - $location = null; + if ($location === null) { + $location = 'files'; } if (! $quality && $this->config instanceof Config) { From e2fc16276d8bc97c96514f969e2bd2a135dedf5c Mon Sep 17 00:00:00 2001 From: scouzinier Date: Tue, 5 May 2026 08:24:42 +0200 Subject: [PATCH 04/18] fix ecs check --- src/Controller/ImageController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Controller/ImageController.php b/src/Controller/ImageController.php index aca0b36ed..4cecb45ba 100644 --- a/src/Controller/ImageController.php +++ b/src/Controller/ImageController.php @@ -214,7 +214,7 @@ private function parseParameters(string $paramString): void $remaining = array_values(array_filter( array_slice($raw, 2), - static fn ($value): bool => $value !== null && $value !== '' + static fn (int|string $value): bool => $value !== '' )); if (isset($remaining[0]) && is_numeric($remaining[0]) && 0 <= (int) $remaining[0] && (int) $remaining[0] <= 100) { @@ -289,7 +289,7 @@ private function parameterPath(): string $this->parameters['q'] ?? 80, $this->parameters['fit'] ?? null, $this->parameters['location'] ?? 'files', - ], fn ($v) => $v !== null && $v !== '' && $v !== 0); + ], fn (int|string|null $v): bool => $v !== null && $v !== '' && $v !== 0); return implode('×', $parts); } From d71237062b36905f816f6dc3c077262f35774f65 Mon Sep 17 00:00:00 2001 From: scouzinier Date: Tue, 5 May 2026 08:39:03 +0200 Subject: [PATCH 05/18] fix phpstan --- src/Controller/ImageController.php | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/src/Controller/ImageController.php b/src/Controller/ImageController.php index 4cecb45ba..f2134dcc6 100644 --- a/src/Controller/ImageController.php +++ b/src/Controller/ImageController.php @@ -172,36 +172,9 @@ private function buildResponse(Request $request, string $filename): Response } } - private function parseParametersold(string $paramString): void - { - $raw = explode('×', (string) preg_replace('/([0-9])(x)([0-9a-z])/i', '\1×\3', $paramString)); - - $this->parameters = [ - 'w' => (isset($raw[0]) && is_numeric($raw[0])) ? (int) $raw[0] : 400, - 'h' => (isset($raw[1]) && is_numeric($raw[1])) ? (int) $raw[1] : 300, - 'fit' => $raw[2] ?? $this->config->get('general/thumbnails/default_cropping', 'default'), - 'location' => 'files', - 'q' => (! empty($raw[2]) && 0 <= $raw[2] && $raw[2] <= 100) ? (int) $raw[2] : 80, - ]; - - if (isset($raw[4])) { - $this->parameters['fit'] = $this->parseFit($raw[3]); - $this->parameters['location'] = $raw[4]; - } elseif (isset($raw[3])) { - $possibleFit = $this->parseFit($raw[3]); - - if ($this->testFit($possibleFit)) { - $this->parameters['fit'] = $possibleFit; - } else { - $this->parameters['location'] = $raw[3]; - } - } - } - private function parseParameters(string $paramString): void { $raw = explode('×', (string) preg_replace('/([0-9])(x)([0-9a-z])/i', '\1×\3', $paramString)); - $defaultFit = $this->config->get('general/thumbnails/default_cropping', 'default'); $this->parameters = [ 'w' => (isset($raw[0]) && is_numeric($raw[0])) ? (int) $raw[0] : 400, From c4ed3a061b6fe2eaf64634978ff556e3584c667f Mon Sep 17 00:00:00 2001 From: scouzinier Date: Sun, 10 May 2026 11:39:02 +0200 Subject: [PATCH 06/18] fix traversal --- src/Controller/ImageController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Controller/ImageController.php b/src/Controller/ImageController.php index f2134dcc6..53c2eeb48 100644 --- a/src/Controller/ImageController.php +++ b/src/Controller/ImageController.php @@ -48,7 +48,6 @@ public function thumbnail(Request $request, string $paramString, string $filenam } $this->parseParameters($paramString); - $urlFilename = $filename; $sourceFilename = $this->parseFormatFromFilename($filename); try { @@ -57,6 +56,8 @@ public function thumbnail(Request $request, string $paramString, string $filenam return $this->sendErrorImage(); } + $urlFilename = $this->parameters['fm'] !== '' ? $sourceFilename . '.' . $this->parameters['fm'] : $sourceFilename; + $this->createServer($request); $this->saveThumb($request, $sourceFilename, $urlFilename); From 1b1eb1ad3a59d86016f3cbff1cb29cda9813587b Mon Sep 17 00:00:00 2001 From: scouzinier Date: Sun, 10 May 2026 16:15:06 +0200 Subject: [PATCH 07/18] add default value for fit/quality --- src/Controller/ImageController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Controller/ImageController.php b/src/Controller/ImageController.php index 53c2eeb48..287136e2b 100644 --- a/src/Controller/ImageController.php +++ b/src/Controller/ImageController.php @@ -181,9 +181,9 @@ private function parseParameters(string $paramString): void 'w' => (isset($raw[0]) && is_numeric($raw[0])) ? (int) $raw[0] : 400, 'h' => (isset($raw[1]) && is_numeric($raw[1])) ? (int) $raw[1] : 300, 'fm' => '', - 'fit' => '', + 'fit' => $this->config->get('general/thumbnails/default_cropping', 'default'), 'location' => 'files', - 'q' => 80, + 'q' => $this->config->get('general/thumbnails/quality', 80) ]; $remaining = array_values(array_filter( From 41e21a0191441addefd2e9638ffbcae6c4b763a2 Mon Sep 17 00:00:00 2001 From: scouzinier Date: Sun, 10 May 2026 16:16:30 +0200 Subject: [PATCH 08/18] add default fit value in thumb url --- src/Utils/ThumbnailHelper.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Utils/ThumbnailHelper.php b/src/Utils/ThumbnailHelper.php index 33a17b932..f222b30b6 100644 --- a/src/Utils/ThumbnailHelper.php +++ b/src/Utils/ThumbnailHelper.php @@ -35,6 +35,10 @@ private function parameters(?int $width = null, ?int $height = null, ?string $fi $quality = (int) $this->config->get('general/thumbnails/quality'); } + if ($fit === null && $this->config instanceof Config) { + $fit = $this->config->get('general/thumbnails/default_cropping', 'default'); + } + return implode('×', array_filter([$width, $height, $quality, $fit, $location])); } From 5b7f93ccf195e9f527d4360a2010e77a613a7784 Mon Sep 17 00:00:00 2001 From: scouzinier Date: Mon, 11 May 2026 09:31:38 +0200 Subject: [PATCH 09/18] Prevent user to create/delete folder/files anywhere (#3717) * Using PathCanonicalize::canonicalize() instead of Symfony's Path::canonicalize() closes a path traversal vulnerability. * fix rector * fix BadRequestHttpException --- src/Controller/Backend/FilemanagerController.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Controller/Backend/FilemanagerController.php b/src/Controller/Backend/FilemanagerController.php index 6374aaa69..b2a5ec30c 100644 --- a/src/Controller/Backend/FilemanagerController.php +++ b/src/Controller/Backend/FilemanagerController.php @@ -20,6 +20,7 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; @@ -96,11 +97,15 @@ public function delete(Request $request): Response $path = $this->getFromRequest($request, 'path'); $location = $this->getFromRequest($request, 'location'); + if (! is_string($path)) { + throw new BadRequestHttpException('Invalid filename'); + } + $this->denyAccessUnlessGranted('managefiles:' . $location); $location = $this->fileLocations->get($location); - $folder = Path::canonicalize($location->getBasepath() . '/' . $path); + $folder = PathCanonicalize::canonicalize($location->getBasepath(), $path); if (! $this->filesystem->exists($folder)) { $this->addFlash('warning', 'filemanager.delete_folder_missing'); @@ -139,7 +144,7 @@ public function create(Request $request): Response $location = $this->fileLocations->get($location); - $folder = Path::canonicalize($location->getBasepath() . '/' . $path); + $folder = PathCanonicalize::canonicalize($location->getBasepath(), $path); if ($this->filesystem->exists($folder)) { $this->addFlash('warning', 'filemanager.create_folder_already_exists'); From af0a675ff3855476e890a5267266ff6f1181798f Mon Sep 17 00:00:00 2001 From: scouzinier Date: Mon, 11 May 2026 11:31:40 +0200 Subject: [PATCH 10/18] fix phpstan --- src/Controller/ImageController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Controller/ImageController.php b/src/Controller/ImageController.php index 287136e2b..8f371a504 100644 --- a/src/Controller/ImageController.php +++ b/src/Controller/ImageController.php @@ -29,6 +29,7 @@ class ImageController * w?: int, * h?: int, * fit?: string, + * fm?: string, * location?: string, * q?: int * } @@ -56,7 +57,7 @@ public function thumbnail(Request $request, string $paramString, string $filenam return $this->sendErrorImage(); } - $urlFilename = $this->parameters['fm'] !== '' ? $sourceFilename . '.' . $this->parameters['fm'] : $sourceFilename; + $urlFilename = isset($this->parameters['fm']) && $this->parameters['fm'] !== '' ? $sourceFilename . '.' . $this->parameters['fm']:$sourceFilename; $this->createServer($request); $this->saveThumb($request, $sourceFilename, $urlFilename); From 2810f68721b2ad655687a9b4e904a4df7c865e1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 20:16:51 +0000 Subject: [PATCH 11/18] Bump systeminformation from 5.31.1 to 5.31.6 Bumps [systeminformation](https://github.com/sebhildebrandt/systeminformation) from 5.31.1 to 5.31.6. - [Release notes](https://github.com/sebhildebrandt/systeminformation/releases) - [Changelog](https://github.com/sebhildebrandt/systeminformation/blob/master/CHANGELOG.md) - [Commits](https://github.com/sebhildebrandt/systeminformation/compare/v5.31.1...v5.31.6) --- updated-dependencies: - dependency-name: systeminformation dependency-version: 5.31.6 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5842b7d66..38b49ef60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22535,9 +22535,9 @@ } }, "node_modules/systeminformation": { - "version": "5.31.1", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.1.tgz", - "integrity": "sha512-6pRwxoGeV/roJYpsfcP6tN9mep6pPeCtXbUOCdVa0nme05Brwcwdge/fVNhIZn2wuUitAKZm4IYa7QjnRIa9zA==", + "version": "5.31.6", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.6.tgz", + "integrity": "sha512-Uv2b2uGGM6ns+26czgW2cYRabYdnswM0ddSOOlryHOaelzsmDSet1iM/NT7VOYxW8x/BW+HkY+b1Ve2pLTSGSA==", "dev": true, "license": "MIT", "os": [ From f03121f257fb03a88a3583e322d6bbe9f92af027 Mon Sep 17 00:00:00 2001 From: Bob van de Vijver Date: Thu, 14 May 2026 17:32:48 +0200 Subject: [PATCH 12/18] Add missing release notes --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 432c5fa3a..43b8de8ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,21 @@ Changelog ========= +## 6.1.2 + +Released: 2026-04-27 + +Bugfix release for MySQL platform. + - [#3710](https://github.com/bolt/core/pull/3710) + - [#3713](https://github.com/bolt/core/pull/3713) + +## 6.1.1 + +Released: 2026-04-24 + +Maintenance release bumping some dependencies. +Bolt now also disables the save button during form submission. + ## 6.1.0 Released: 2026-04-09 From 4a6e9ab6bffb5ba34c9b92d628b60e6d62c1639f Mon Sep 17 00:00:00 2001 From: Bob van de Vijver Date: Thu, 14 May 2026 17:33:32 +0200 Subject: [PATCH 13/18] Switch to SVG-sanitation library to sanitise uploaded SVG files --- composer.json | 1 + phpstan-baseline.php | 14 +--------- .../Backend/Async/UploadController.php | 26 ++++++++++++++----- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/composer.json b/composer.json index e9fc7d417..b78149b36 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "doctrine/orm": "^3.5", "drupol/composer-packages": "^2.0", "embed/embed": "^4.4", + "enshrined/svg-sanitize": "^0.22.0", "erusev/parsedown": "^1.8", "erusev/parsedown-extra": "^0.9", "fakerphp/faker": "^1.16", diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 2e9d4aef6..ab0883bc4 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -1076,23 +1076,11 @@ 'path' => __DIR__ . '/src/Controller/Backend/Async/UploadController.php', ]; $ignoreErrors[] = [ - 'message' => '#^Method Bolt\\\\Controller\\\\Backend\\\\Async\\\\UploadController\\:\\:checkJavascriptInSVG\\(\\) has parameter \\$file with no value type specified in iterable type array\\.$#', + 'message' => '#^Method Bolt\\\\Controller\\\\Backend\\\\Async\\\\UploadController\\:\\:sanitizeSvgContent\\(\\) has parameter \\$file with no value type specified in iterable type array\\.$#', 'identifier' => 'missingType.iterableValue', 'count' => 1, 'path' => __DIR__ . '/src/Controller/Backend/Async/UploadController.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#1 \\$string of function mb_strtolower expects string, string\\|false given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/src/Controller/Backend/Async/UploadController.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Parameter \\#2 \\$subject of function preg_match expects string, string\\|false given\\.$#', - 'identifier' => 'argument.type', - 'count' => 1, - 'path' => __DIR__ . '/src/Controller/Backend/Async/UploadController.php', -]; $ignoreErrors[] = [ 'message' => '#^Method Bolt\\\\Controller\\\\Backend\\\\BulkOperationsController\\:\\:findRecordsFromIds\\(\\) has parameter \\$ids with no value type specified in iterable type array\\.$#', 'identifier' => 'missingType.iterableValue', diff --git a/src/Controller/Backend/Async/UploadController.php b/src/Controller/Backend/Async/UploadController.php index b1445d2ac..389ef159e 100644 --- a/src/Controller/Backend/Async/UploadController.php +++ b/src/Controller/Backend/Async/UploadController.php @@ -11,6 +11,7 @@ use Bolt\Twig\TextExtension; use Cocur\Slugify\Slugify; use Doctrine\ORM\EntityManagerInterface; +use enshrined\svgSanitize\Sanitizer; use Sirius\Upload\Handler; use Sirius\Upload\Result\File; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -150,8 +151,8 @@ public function handleUpload(Request $request): JsonResponse $uploadHandler->addRule( 'callback', - ['callback' => $this->checkJavascriptInSVG(...)], - 'It is not allowed to upload SVG\'s with embedded Javascript.', + ['callback' => $this->sanitizeSvgContent(...)], + 'The SVG-file could not be sanitized automatically, is it a valid SVG-file?', 'Upload file' ); @@ -210,18 +211,31 @@ private function sanitiseFilename(string $filename): string return $filename . '.' . $extension; } - public function checkJavascriptInSVG(array $file): bool + public function sanitizeSvgContent(array $file): bool { if (Path::getExtension($file['name']) != 'svg') { return true; } - $svgFile = file_get_contents($file['tmp_name']); + // Configure sanitizer + $sanitizer = new Sanitizer(); + $sanitizer->minify(true); + $sanitizer->removeXMLTag(true); + $sanitizer->removeRemoteReferences(true); - if (preg_match('/(?:<[^>]+\s)(on\S+)=["\']?((?:.(?!["\']?\s+(?:\S+)=|[>"\']))+.)["\']?/i', $svgFile)) { + // Retrieve file contents + if (! $svgFile = file_get_contents($file['tmp_name'])) { return false; } - return mb_strpos((string) preg_replace('/\s+/', '', mb_strtolower($svgFile)), 'sanitize($svgFile)) { + return false; + } + + // Write the sanitized SVG back to the temporary file + file_put_contents($file['tmp_name'], $sanitizedSvg); + + return true; } } From da6f8afa316e74e2c74c7d520dbe15403fd84d34 Mon Sep 17 00:00:00 2001 From: Bob van de Vijver Date: Thu, 14 May 2026 21:28:05 +0200 Subject: [PATCH 14/18] Enable SYMFONY_TRUSTED_HOSTS configuration by default Backport the default value from Symfony 7.1+ for easy configuration against Host Header Injection --- .env | 4 ++++ config/packages/framework.yaml | 3 ++- yaml-migrations/m_2026-05-14-framework.yaml | 7 +++++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 yaml-migrations/m_2026-05-14-framework.yaml diff --git a/.env b/.env index 248254344..a207060a6 100644 --- a/.env +++ b/.env @@ -60,3 +60,7 @@ MAILER_DSN=smtp://localhost # Set canonical in the general config. Keep empty to not use it. BOLT_CANONICAL= + +# Ensure you set your trusted hosts to prevent Host Header Injection +# See https://symfony.com/doc/current/reference/configuration/framework.html#trusted-hosts +#SYMFONY_TRUSTED_HOSTS='^example\.com$' diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index 00f1e2b0a..5197c7203 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -3,7 +3,8 @@ framework: secret: '%env(APP_SECRET)%' csrf_protection: { enabled: true } http_method_override: true - trusted_hosts: ~ + trusted_hosts: + - '%env(default::SYMFONY_TRUSTED_HOSTS)%' # Enables session support. Note that the session will ONLY be started if you read or write from it. # Remove or comment this section to explicitly disable session support. diff --git a/yaml-migrations/m_2026-05-14-framework.yaml b/yaml-migrations/m_2026-05-14-framework.yaml new file mode 100644 index 000000000..6e6e15b58 --- /dev/null +++ b/yaml-migrations/m_2026-05-14-framework.yaml @@ -0,0 +1,7 @@ +file: packages/framework.yaml +since: 6.1.3 + +add: + framework: + trusted_hosts: + - '%env(default::SYMFONY_TRUSTED_HOSTS)%' From ec33d5e914b1b66d991bd2f8a38bde70f4695752 Mon Sep 17 00:00:00 2001 From: Bob van de Vijver Date: Sat, 16 May 2026 21:02:43 +0200 Subject: [PATCH 15/18] Create changelog for 6.1.3 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43b8de8ab..04b998e49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,18 @@ Changelog ========= +## 6.1.3 + +Released: 2026-05-16 + +This release includes security-related fixes. Our thanks to @chndlrx and @kouz75 for identifying these issues and disclosing them to us responsibly! 👏🙏 + +### 🔐 Security related changes + +- Prevent user to create/delete folder/files anywhere (@kouz75, https://github.com/bolt/core/pull/3717) +- Switch to SVG-sanitation library to sanitise uploaded SVG files (@bobvandevijver, https://github.com/bolt/core/pull/3723) +- Enable SYMFONY_TRUSTED_HOSTS configuration by default (@bobvandevijver, https://github.com/bolt/core/pull/3723) + ## 6.1.2 Released: 2026-04-27 From fbf648af06890cb42af02c3ec8afd2f07c9adfd3 Mon Sep 17 00:00:00 2001 From: bobvandevijver <1835343+bobvandevijver@users.noreply.github.com> Date: Sat, 16 May 2026 19:34:32 +0000 Subject: [PATCH 16/18] Release 6.1.3 --- assets/js/version.js | 2 +- package.json | 2 +- src/Version.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/js/version.js b/assets/js/version.js index 7d1f5d2e1..f946659d5 100644 --- a/assets/js/version.js +++ b/assets/js/version.js @@ -1,2 +1,2 @@ // generated by genversion -export const version = '6.1.2'; +export const version = '6.1.3'; diff --git a/package.json b/package.json index 3c72130de..808fddc61 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bolt", - "version": "6.1.2", + "version": "6.1.3", "homepage": "https://boltcms.io", "author": "Bob den Otter (https://boltcms.io)", "license": "MIT", diff --git a/src/Version.php b/src/Version.php index 473e9f7a0..effa27299 100644 --- a/src/Version.php +++ b/src/Version.php @@ -23,7 +23,7 @@ final class Version * Stable — 3.0.0 * Development — 3.1.0 alpha 1 */ - public const VERSION = '6.1.2'; + public const VERSION = '6.1.3'; public const CODENAME = ''; From bd53344154af752803575bd47e6fe2c2fa6b8f03 Mon Sep 17 00:00:00 2001 From: scouzinier Date: Mon, 18 May 2026 18:06:45 +0200 Subject: [PATCH 17/18] check filename before using it --- src/Controller/ImageController.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Controller/ImageController.php b/src/Controller/ImageController.php index 8f371a504..f3fb3a40d 100644 --- a/src/Controller/ImageController.php +++ b/src/Controller/ImageController.php @@ -49,14 +49,15 @@ public function thumbnail(Request $request, string $paramString, string $filenam } $this->parseParameters($paramString); - $sourceFilename = $this->parseFormatFromFilename($filename); try { - $sourceFilename = PathCanonicalize::canonicalize($this->getPath($request), $sourceFilename, true); + $filename = PathCanonicalize::canonicalize($this->getPath($request), $filename, true); } catch (Exception) { return $this->sendErrorImage(); } + $sourceFilename = $this->parseFormatFromFilename($filename); + $urlFilename = isset($this->parameters['fm']) && $this->parameters['fm'] !== '' ? $sourceFilename . '.' . $this->parameters['fm']:$sourceFilename; $this->createServer($request); From f4f2ad80e58efedea345bb9fe92c4e2ec788354c Mon Sep 17 00:00:00 2001 From: scouzinier Date: Mon, 18 May 2026 18:08:24 +0200 Subject: [PATCH 18/18] check filename before using it --- src/Controller/ImageController.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Controller/ImageController.php b/src/Controller/ImageController.php index f3fb3a40d..6bd8378f0 100644 --- a/src/Controller/ImageController.php +++ b/src/Controller/ImageController.php @@ -92,10 +92,16 @@ private function getPath(Request $request, ?string $path = null, bool $absolute private function parseFormatFromFilename(string $filename): string { - $ext = mb_strtolower(pathinfo($filename, PATHINFO_EXTENSION)); - if ($this->isSupportedFormat($ext) && pathinfo(pathinfo($filename, PATHINFO_FILENAME), PATHINFO_EXTENSION) !== '') { - $this->parameters['fm'] = $ext; + $parts = explode('.', pathinfo($filename, PATHINFO_BASENAME)); + + if (count($parts) < 3) { + return $filename; + } + $ext = mb_strtolower(end($parts)); + + if ($this->isSupportedFormat($ext)) { + $this->parameters['fm'] = $ext; return mb_substr($filename, 0, -(mb_strlen($ext) + 1)); }