diff --git a/composer.json b/composer.json index c725b235..e57ce652 100644 --- a/composer.json +++ b/composer.json @@ -43,6 +43,7 @@ "omines/datatables-bundle": "^0.9.1", "paragonie/sodium_compat": "^1.21", "part-db/label-fonts": "^1.0", + "rhukster/dom-sanitizer": "^1.0", "runtime/frankenphp-symfony": "^0.2.0", "s9e/text-formatter": "^2.1", "scheb/2fa-backup-code": "^6.8.0", diff --git a/composer.lock b/composer.lock index ae1684cf..0337025b 100644 --- a/composer.lock +++ b/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": "7fb73581b0074c5a79afb3ffa614ed8e", + "content-hash": "27cd0d915eab5e7cb57215a4c0b529fb", "packages": [ { "name": "amphp/amp", @@ -7125,6 +7125,51 @@ }, "time": "2025-01-25T19:27:39+00:00" }, + { + "name": "rhukster/dom-sanitizer", + "version": "1.0.7", + "source": { + "type": "git", + "url": "https://github.com/rhukster/dom-sanitizer.git", + "reference": "c2a98f27ad742668b254282ccc5581871d0fb601" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rhukster/dom-sanitizer/zipball/c2a98f27ad742668b254282ccc5581871d0fb601", + "reference": "c2a98f27ad742668b254282ccc5581871d0fb601", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9" + }, + "type": "library", + "autoload": { + "psr-4": { + "Rhukster\\DomSanitizer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andy Miller", + "email": "rhuk@rhuk.net" + } + ], + "description": "A simple but effective DOM/SVG/MathML Sanitizer for PHP 7.4+", + "support": { + "issues": "https://github.com/rhukster/dom-sanitizer/issues", + "source": "https://github.com/rhukster/dom-sanitizer/tree/1.0.7" + }, + "time": "2023-11-06T16:46:48+00:00" + }, { "name": "robrichards/xmlseclibs", "version": "3.1.3", diff --git a/src/Services/Attachments/AttachmentSubmitHandler.php b/src/Services/Attachments/AttachmentSubmitHandler.php index 60849aa0..b5b78049 100644 --- a/src/Services/Attachments/AttachmentSubmitHandler.php +++ b/src/Services/Attachments/AttachmentSubmitHandler.php @@ -65,7 +65,7 @@ class AttachmentSubmitHandler 'htpasswd', '']; public function __construct(protected AttachmentPathResolver $pathResolver, protected bool $allow_attachments_downloads, - protected HttpClientInterface $httpClient, protected MimeTypesInterface $mimeTypes, + protected HttpClientInterface $httpClient, protected MimeTypesInterface $mimeTypes, protected readonly SVGSanitizer $SVGSanitizer, protected FileTypeFilterTools $filterTools, /** * @var string The user configured maximum upload size. This is a string like "10M" or "1G" and will be converted to */ @@ -214,6 +214,9 @@ class AttachmentSubmitHandler //Move the attachment files to secure location (and back) if needed $this->moveFile($attachment, $secure_attachment); + //Sanitize the SVG if needed + $this->sanitizeSVGFiles($attachment); + //Rename blacklisted (unsecure) files to a better extension $this->renameBlacklistedExtensions($attachment); @@ -498,4 +501,32 @@ class AttachmentSubmitHandler return $this->max_upload_size_bytes; } + + /** + * Sanatizes the given SVG file, if the attachment is an internal SVG file. + * @param Attachment $attachment + * @return Attachment + */ + protected function sanitizeSVGFiles(Attachment $attachment): Attachment + { + //We can not do anything on builtins or external ressources + if ($attachment->isBuiltIn() || !$attachment->hasInternal()) { + return $attachment; + } + + //Resolve the path to the file + $path = $this->pathResolver->placeholderToRealPath($attachment->getInternalPath()); + + //Check if the file exists + if (!file_exists($path)) { + return $attachment; + } + + //Check if the file is an SVG + if ($attachment->getExtension() === "svg") { + $this->SVGSanitizer->sanitizeFile($path); + } + + return $attachment; + } } diff --git a/src/Services/Attachments/SVGSanitizer.php b/src/Services/Attachments/SVGSanitizer.php new file mode 100644 index 00000000..9ac5956b --- /dev/null +++ b/src/Services/Attachments/SVGSanitizer.php @@ -0,0 +1,58 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services\Attachments; + +use Rhukster\DomSanitizer\DOMSanitizer; + +class SVGSanitizer +{ + + /** + * Sanitizes the given SVG string by removing any potentially harmful content (like inline scripts). + * @param string $input + * @return string + */ + public function sanitizeString(string $input): string + { + return (new DOMSanitizer(DOMSanitizer::SVG))->sanitize($input); + } + + /** + * Sanitizes the given SVG file by removing any potentially harmful content (like inline scripts). + * The sanitized content is written back to the file. + * @param string $filepath + */ + public function sanitizeFile(string $filepath): void + { + //Open the file and read the content + $content = file_get_contents($filepath); + if ($content === false) { + throw new \RuntimeException('Could not read file: ' . $filepath); + } + //Sanitize the content + $sanitizedContent = $this->sanitizeString($content); + //Write the sanitized content back to the file + file_put_contents($filepath, $sanitizedContent); + } +} \ No newline at end of file