From bf7fedf2086d86aac16194733a6385564e5cf124 Mon Sep 17 00:00:00 2001 From: antoine Date: Thu, 7 Aug 2025 18:48:36 +0200 Subject: [PATCH] Sanitize SVG --- composer.json | 1 + demo/composer.json | 1 + demo/composer.lock | 49 ++++++++++++++++++- .../Fields/Formatters/UploadFormatter.php | 1 + src/Form/Fields/SharpFormUploadField.php | 13 +++++ src/Http/Jobs/HandleUploadedFileJob.php | 14 ++++-- src/Http/Jobs/SanitizeSvgJob.php | 35 +++++++++++++ src/Utils/Uploads/SharpUploadManager.php | 2 + tests-e2e/site/composer.json | 1 + tests-e2e/site/composer.lock | 47 +++++++++++++++++- tests/Http/Jobs/HandleUploadedFileJobTest.php | 43 ++++++++++++++-- 11 files changed, 198 insertions(+), 9 deletions(-) create mode 100644 src/Http/Jobs/SanitizeSvgJob.php diff --git a/composer.json b/composer.json index 8b5cf8e5f..66c1f8574 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "ext-mbstring": "*", "blade-ui-kit/blade-icons": "^1.6", "code16/laravel-content-renderer": "^1.1", + "enshrined/svg-sanitize": "^0.21.0", "inertiajs/inertia-laravel": "^2.0", "intervention/image": "^3.4", "laravel/framework": "^11.0|^12.0", diff --git a/demo/composer.json b/demo/composer.json index a63bdff04..15f6e5440 100644 --- a/demo/composer.json +++ b/demo/composer.json @@ -6,6 +6,7 @@ "bacon/bacon-qr-code": "~2.0", "blade-ui-kit/blade-icons": "^1.6", "code16/laravel-content-renderer": "^1.2", + "enshrined/svg-sanitize": "^0.21.0", "guzzlehttp/guzzle": "^7.2", "inertiajs/inertia-laravel": "^2.0", "intervention/image": "^3.4", diff --git a/demo/composer.lock b/demo/composer.lock index 72ab95e1b..52e2c1684 100644 --- a/demo/composer.lock +++ b/demo/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c48652e31ba9f176b3c362067dda8c32", + "content-hash": "4d073c5cd646acd5da2c16b5958104c8", "packages": [ { "name": "bacon/bacon-qr-code", @@ -759,6 +759,51 @@ ], "time": "2025-03-06T22:45:56+00:00" }, + { + "name": "enshrined/svg-sanitize", + "version": "0.21.0", + "source": { + "type": "git", + "url": "https://github.com/darylldoyle/svg-sanitizer.git", + "reference": "5e477468fac5c5ce933dce53af3e8e4e58dcccc9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/darylldoyle/svg-sanitizer/zipball/5e477468fac5c5ce933dce53af3e8e4e58dcccc9", + "reference": "5e477468fac5c5ce933dce53af3e8e4e58dcccc9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.5 || ^8.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "enshrined\\svgSanitize\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Daryll Doyle", + "email": "daryll@enshrined.co.uk" + } + ], + "description": "An SVG sanitizer for PHP", + "support": { + "issues": "https://github.com/darylldoyle/svg-sanitizer/issues", + "source": "https://github.com/darylldoyle/svg-sanitizer/tree/0.21.0" + }, + "time": "2025-01-13T09:32:25+00:00" + }, { "name": "fruitcake/php-cors", "version": "v1.3.0", @@ -10513,7 +10558,7 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.2" + "php": "^8.4" }, "platform-dev": {}, "plugin-api-version": "2.6.0" diff --git a/src/Form/Fields/Formatters/UploadFormatter.php b/src/Form/Fields/Formatters/UploadFormatter.php index a83171a7e..3fa53d841 100644 --- a/src/Form/Fields/Formatters/UploadFormatter.php +++ b/src/Form/Fields/Formatters/UploadFormatter.php @@ -55,6 +55,7 @@ public function fromFront(SharpFormField $field, string $attribute, $value): ?ar disk: $field->storageDisk(), filePath: $formatted['file_name'], shouldOptimizeImage: $field->isImageOptimize(), + shouldSanitizeSvg: $field->isImageSanitizeSvg(), transformFilters: $field->isImageTransformOriginal() ? ($value['filters'] ?? null) : null, diff --git a/src/Form/Fields/SharpFormUploadField.php b/src/Form/Fields/SharpFormUploadField.php index cb9d5358a..c75787688 100644 --- a/src/Form/Fields/SharpFormUploadField.php +++ b/src/Form/Fields/SharpFormUploadField.php @@ -23,6 +23,7 @@ class SharpFormUploadField extends SharpFormField protected ?Dimensions $imageDimensionConstraints = null; protected bool $imageCompactThumbnail = false; protected bool $imageOptimize = false; + protected bool $imageSanitizeSvg = true; protected ?array $imageCropRatio = null; protected ?array $imageTransformableFileTypes = null; @@ -93,6 +94,18 @@ public function isImageOptimize(): bool return $this->imageOptimize; } + public function setImageSanitizeSvg(bool $imageSanitizeSvg = true): self + { + $this->imageSanitizeSvg = $imageSanitizeSvg; + + return $this; + } + + public function isImageSanitizeSvg(): bool + { + return $this->imageSanitizeSvg; + } + public function setImageCompactThumbnail(bool $compactThumbnail = true): self { $this->imageCompactThumbnail = $compactThumbnail; diff --git a/src/Http/Jobs/HandleUploadedFileJob.php b/src/Http/Jobs/HandleUploadedFileJob.php index 1c77b9997..7710d6c9c 100644 --- a/src/Http/Jobs/HandleUploadedFileJob.php +++ b/src/Http/Jobs/HandleUploadedFileJob.php @@ -21,6 +21,7 @@ public function __construct( public string $disk, public string $filePath, public bool $shouldOptimizeImage = true, + public bool $shouldSanitizeSvg = true, public ?array $transformFilters = null, public ?string $instanceId = null, ) {} @@ -45,9 +46,16 @@ public function handle(): void if ($this->transformFilters) { // There are transformation and field was configured to handle transformation on the source image HandleTransformedFileJob::dispatchSync( - $tmpDisk, - $tmpFilePath, - $this->transformFilters + disk: $tmpDisk, + filePath: $tmpFilePath, + transformFilters: $this->transformFilters + ); + } + + if ($this->shouldSanitizeSvg && Storage::disk($tmpDisk)->mimeType($tmpFilePath) === 'image/svg+xml') { + SanitizeSvgJob::dispatchSync( + disk: $tmpDisk, + filePath: $tmpFilePath ); } diff --git a/src/Http/Jobs/SanitizeSvgJob.php b/src/Http/Jobs/SanitizeSvgJob.php new file mode 100644 index 000000000..34b5e42fe --- /dev/null +++ b/src/Http/Jobs/SanitizeSvgJob.php @@ -0,0 +1,35 @@ +minify(true); + $sanitizer->removeXMLTag(true); + $sanitizedSvg = $sanitizer->sanitize( + Storage::disk($this->disk)->get($this->filePath) + ); + + Storage::disk($this->disk) + ->put($this->filePath, $sanitizedSvg); + } +} diff --git a/src/Utils/Uploads/SharpUploadManager.php b/src/Utils/Uploads/SharpUploadManager.php index 2b9efae1f..a2451357e 100644 --- a/src/Utils/Uploads/SharpUploadManager.php +++ b/src/Utils/Uploads/SharpUploadManager.php @@ -30,6 +30,7 @@ public function queueHandleUploadedFile( string $disk, string $filePath, bool $shouldOptimizeImage = true, + bool $shouldSanitizeSvg = true, ?array $transformFilters = null, ): void { $this->uploadedFileQueue[] = compact( @@ -37,6 +38,7 @@ public function queueHandleUploadedFile( 'disk', 'filePath', 'shouldOptimizeImage', + 'shouldSanitizeSvg', 'transformFilters', ); } diff --git a/tests-e2e/site/composer.json b/tests-e2e/site/composer.json index 01072cf0d..ee4c2a410 100644 --- a/tests-e2e/site/composer.json +++ b/tests-e2e/site/composer.json @@ -10,6 +10,7 @@ "ext-json": "*", "blade-ui-kit/blade-icons": "^1.7", "code16/laravel-content-renderer": "^1.2", + "enshrined/svg-sanitize": "^0.21.0", "inertiajs/inertia-laravel": "^2.0", "intervention/image": "^3.9", "intervention/image-laravel": "^1.3", diff --git a/tests-e2e/site/composer.lock b/tests-e2e/site/composer.lock index 98da1007b..4d1421485 100644 --- a/tests-e2e/site/composer.lock +++ b/tests-e2e/site/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0ba11b09307ec53ce84cc1bb4a3f3c67", + "content-hash": "8688d337ab2efe1f3e1bbd9ed0cc9857", "packages": [ { "name": "blade-ui-kit/blade-icons", @@ -700,6 +700,51 @@ ], "time": "2024-12-27T00:36:43+00:00" }, + { + "name": "enshrined/svg-sanitize", + "version": "0.21.0", + "source": { + "type": "git", + "url": "https://github.com/darylldoyle/svg-sanitizer.git", + "reference": "5e477468fac5c5ce933dce53af3e8e4e58dcccc9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/darylldoyle/svg-sanitizer/zipball/5e477468fac5c5ce933dce53af3e8e4e58dcccc9", + "reference": "5e477468fac5c5ce933dce53af3e8e4e58dcccc9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.5 || ^8.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "enshrined\\svgSanitize\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "Daryll Doyle", + "email": "daryll@enshrined.co.uk" + } + ], + "description": "An SVG sanitizer for PHP", + "support": { + "issues": "https://github.com/darylldoyle/svg-sanitizer/issues", + "source": "https://github.com/darylldoyle/svg-sanitizer/tree/0.21.0" + }, + "time": "2025-01-13T09:32:25+00:00" + }, { "name": "fruitcake/php-cors", "version": "v1.3.0", diff --git a/tests/Http/Jobs/HandleUploadedFileJobTest.php b/tests/Http/Jobs/HandleUploadedFileJobTest.php index b87e5dee0..f648ac1a9 100644 --- a/tests/Http/Jobs/HandleUploadedFileJobTest.php +++ b/tests/Http/Jobs/HandleUploadedFileJobTest.php @@ -119,8 +119,45 @@ public function create() ], ); - $this->assertNotEquals( - $originalSize, - Storage::disk('local')->size('data/image.jpg') + expect(Storage::disk('local')->size('data/image.jpg'))->not->toEqual($originalSize); +}); + +it('sanitizes svg files', function () { + UploadedFile::fake() + ->createWithContent( + 'image.svg', + '' + ) + ->storeAs('/tmp', 'image.svg', ['disk' => 'local']); + + HandleUploadedFileJob::dispatch( + uploadedFileName: 'image.svg', + disk: 'local', + filePath: 'data/image.svg', + shouldOptimizeImage: false, + shouldSanitizeSvg: true, ); + + expect(Storage::disk('local')->get('data/image.svg')) + ->toEqual(''); +}); + +it('does not sanitize svg files if not configured', function () { + UploadedFile::fake() + ->createWithContent( + 'image.svg', + '' + ) + ->storeAs('/tmp', 'image.svg', ['disk' => 'local']); + + HandleUploadedFileJob::dispatch( + uploadedFileName: 'image.svg', + disk: 'local', + filePath: 'data/image.svg', + shouldOptimizeImage: false, + shouldSanitizeSvg: false, + ); + + expect(Storage::disk('local')->get('data/image.svg')) + ->toEqual(''); });