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('');
});