diff --git a/composer.json b/composer.json index 435cabe30..122cdebd7 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "require": { "php": "8.2.*|8.3.*|8.4.*", "code16/laravel-content-renderer": "^1.1.0", + "enshrined/svg-sanitize": "^0.21", "intervention/image": "^3.4", "intervention/image-laravel": "^1.2", "laravel/framework": "^10.0|^11.0", diff --git a/src/Form/Fields/Formatters/UploadFormatter.php b/src/Form/Fields/Formatters/UploadFormatter.php index 79ee20cac..12fa4b4a6 100644 --- a/src/Form/Fields/Formatters/UploadFormatter.php +++ b/src/Form/Fields/Formatters/UploadFormatter.php @@ -8,6 +8,7 @@ use Code16\Sharp\Form\Fields\SharpFormUploadField; use Code16\Sharp\Form\Fields\Utils\SharpFormFieldWithUpload; use Code16\Sharp\Utils\FileUtil; +use enshrined\svgSanitize\Sanitizer; use Illuminate\Filesystem\FilesystemManager; use Illuminate\Support\Arr; use Intervention\Image\Image; @@ -80,6 +81,11 @@ public function fromFront(SharpFormField $field, string $attribute, $value): ?ar $storage->put($storedFilePath, $fileContent); + // Sanitize SVG files to prevent XSS attacks + if ($field->isShouldSanitizeSvg() && $storage->mimeType($storedFilePath) === 'image/svg+xml') { + $this->sanitizeSvg($storage, $storedFilePath); + } + return [ 'file_name' => $storedFilePath, 'size' => $storage->size($storedFilePath), @@ -184,4 +190,17 @@ protected function returnAfterNoChangeWasMade(?array $data): ?array ? $data : ($data === null ? null : []); } + + protected function sanitizeSvg($storage, string $filePath): void + { + $sanitizer = new Sanitizer(); + $sanitizer->minify(true); + $sanitizer->removeXMLTag(true); + + $sanitizedSvg = $sanitizer->sanitize($storage->get($filePath)); + + if ($sanitizedSvg !== false) { + $storage->put($filePath, $sanitizedSvg); + } + } } diff --git a/src/Form/Fields/Utils/SharpFormFieldWithUpload.php b/src/Form/Fields/Utils/SharpFormFieldWithUpload.php index c6be5e2f4..e06433dce 100644 --- a/src/Form/Fields/Utils/SharpFormFieldWithUpload.php +++ b/src/Form/Fields/Utils/SharpFormFieldWithUpload.php @@ -15,6 +15,7 @@ trait SharpFormFieldWithUpload protected ?bool $transformKeepOriginal = null; protected bool $compactThumbnail = false; protected bool $shouldOptimizeImage = false; + protected bool $shouldSanitizeSvg = true; protected string|array|null $fileFilter = null; public function setMaxFileSize(float $maxFileSizeInMB): self @@ -52,6 +53,18 @@ public function isShouldOptimizeImage(): bool return $this->shouldOptimizeImage; } + public function setSanitizeSvg(bool $shouldSanitizeSvg = true): self + { + $this->shouldSanitizeSvg = $shouldSanitizeSvg; + + return $this; + } + + public function isShouldSanitizeSvg(): bool + { + return $this->shouldSanitizeSvg; + } + public function setCompactThumbnail(bool $compactThumbnail = true): self { $this->compactThumbnail = $compactThumbnail; diff --git a/tests/Unit/Form/Fields/Formatters/UploadFormatterTest.php b/tests/Unit/Form/Fields/Formatters/UploadFormatterTest.php index d5fa4c3bb..ad7727b67 100644 --- a/tests/Unit/Form/Fields/Formatters/UploadFormatterTest.php +++ b/tests/Unit/Form/Fields/Formatters/UploadFormatterTest.php @@ -355,4 +355,56 @@ public function we_return_full_object_after_only_transformations_if_configured() ), ); } + + /** @test */ + public function we_sanitize_svg_files_by_default() + { + $maliciousSvg = ''; + + Storage::disk('local')->put('/tmp/malicious.svg', $maliciousSvg); + + $field = SharpFormUploadField::make('upload') + ->setStorageBasePath('data') + ->setStorageDisk('local'); + + (new UploadFormatter()) + ->fromFront( + $field, + 'attribute', + [ + 'name' => 'malicious.svg', + 'uploaded' => true, + ], + ); + + $sanitizedContent = Storage::disk('local')->get('data/malicious.svg'); + $this->assertStringNotContainsString(''; + + Storage::disk('local')->put('/tmp/test.svg', $svgWithScript); + + $field = SharpFormUploadField::make('upload') + ->setStorageBasePath('data') + ->setStorageDisk('local') + ->setSanitizeSvg(false); + + (new UploadFormatter()) + ->fromFront( + $field, + 'attribute', + [ + 'name' => 'test.svg', + 'uploaded' => true, + ], + ); + + $content = Storage::disk('local')->get('data/test.svg'); + $this->assertStringContainsString('