diff options
| author | Greg Roach <greg@subaqua.co.uk> | 2026-04-27 22:02:50 +0100 |
|---|---|---|
| committer | Greg Roach <greg@subaqua.co.uk> | 2026-04-27 22:33:02 +0100 |
| commit | d32e3cfb19891929a9c842586e92370438e27ae7 (patch) | |
| tree | 22c34f5f4920f0cae1f81a2b4dad075182ca0ed7 /tests | |
| parent | 751c9906a0e1029bd558c4223ff05c74f5f9753a (diff) | |
| download | webtrees-d32e3cfb19891929a9c842586e92370438e27ae7.tar.gz webtrees-d32e3cfb19891929a9c842586e92370438e27ae7.tar.bz2 webtrees-d32e3cfb19891929a9c842586e92370438e27ae7.zip | |
Add improved checks for malicious SVG image files
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/app/Factories/ImageFactoryTest.php | 133 | ||||
| -rw-r--r-- | tests/data/media/safe.svg | 1 | ||||
| -rw-r--r-- | tests/data/media/unsafe.svg | 1 |
3 files changed, 135 insertions, 0 deletions
diff --git a/tests/app/Factories/ImageFactoryTest.php b/tests/app/Factories/ImageFactoryTest.php index 3f77d78c1c..bfe43cfffd 100644 --- a/tests/app/Factories/ImageFactoryTest.php +++ b/tests/app/Factories/ImageFactoryTest.php @@ -21,8 +21,13 @@ namespace Fisharebest\Webtrees\Factories; use Fisharebest\Webtrees\Services\PhpService; use Fisharebest\Webtrees\TestCase; +use League\Flysystem\Filesystem; +use League\Flysystem\FilesystemOperator; +use League\Flysystem\Local\LocalFilesystemAdapter; use PHPUnit\Framework\Attributes\CoversClass; +use function dirname; + #[CoversClass(ImageFactory::class)] class ImageFactoryTest extends TestCase { @@ -38,4 +43,132 @@ class ImageFactoryTest extends TestCase $response->getHeaderLine('content-security-policy'), ); } + + public function testFileResponseAddsDownloadHeaderForSafeSvg(): void + { + $php_service = $this->createStub(PhpService::class); + $image_factory = new ImageFactory($php_service); + $filesystem = $this->mediaFilesystem(); + + $php_service + ->method('extensionLoaded') + ->willReturnCallback(static fn (string $extension): bool => $extension === 'dom'); + + $response = $image_factory->fileResponse( + filesystem: $filesystem, + path: 'safe.svg', + download: true, + ); + + self::assertSame('image/svg+xml', $response->getHeaderLine('content-type')); + self::assertSame('default-src none', $response->getHeaderLine('content-security-policy')); + self::assertSame('attachment; filename="safe.svg"', $response->getHeaderLine('content-disposition')); + } + + public function testFileResponseBlocksSvgWithActiveContent(): void + { + $php_service = $this->createStub(PhpService::class); + $image_factory = new ImageFactory($php_service); + $filesystem = $this->mediaFilesystem(); + + $php_service + ->method('extensionLoaded') + ->willReturnCallback(static fn (string $extension): bool => $extension === 'dom'); + + $response = $image_factory->fileResponse( + filesystem: $filesystem, + path: 'unsafe.svg', + download: false, + ); + + self::assertSame('image/svg+xml', $response->getHeaderLine('content-type')); + self::assertSame('SVG image blocked due to XSS.', $response->getHeaderLine('x-image-exception')); + } + + public function testFileResponseBlocksSvgWithoutDomExtension(): void + { + $php_service = $this->createStub(PhpService::class); + $image_factory = new ImageFactory($php_service); + $filesystem = $this->mediaFilesystem(); + + $php_service + ->method('extensionLoaded') + ->willReturnCallback(static fn (string $extension): bool => false); + + $response = $image_factory->fileResponse( + filesystem: $filesystem, + path: 'safe.svg', + download: false, + ); + + self::assertSame('image/svg+xml', $response->getHeaderLine('content-type')); + self::assertSame( + 'Need the PHP dom extension to verify SVG files.', + $response->getHeaderLine('x-image-exception'), + ); + } + + public function testFileResponseReturnsNotFoundForMissingFile(): void + { + $image_factory = new ImageFactory(new PhpService()); + $filesystem = $this->mediaFilesystem(); + + $response = $image_factory->fileResponse( + filesystem: $filesystem, + path: 'missing.svg', + download: false, + ); + + self::assertSame('image/svg+xml', $response->getHeaderLine('content-type')); + self::assertStringContainsString( + 'UnableToReadFile', + $response->getHeaderLine('x-thumbnail-exception'), + ); + } + + public function testThumbnailResponseReturnsImageForJpeg(): void + { + $image_factory = new ImageFactory(new PhpService()); + $filesystem = $this->mediaFilesystem(); + + $response = $image_factory->thumbnailResponse( + filesystem: $filesystem, + path: 'Elizabeth_II.jpg', + width: 40, + height: 40, + fit: 'contain', + ); + + self::assertSame('image/jpeg', $response->getHeaderLine('content-type')); + self::assertSame('default-src none', $response->getHeaderLine('content-security-policy')); + self::assertSame('', $response->getHeaderLine('content-disposition')); + self::assertNotSame('', $response->getBody()->getContents()); + } + + public function testThumbnailResponseReturnsNotFoundForMissingFile(): void + { + $image_factory = new ImageFactory(new PhpService()); + $filesystem = $this->mediaFilesystem(); + + $response = $image_factory->thumbnailResponse( + filesystem: $filesystem, + path: 'missing.jpg', + width: 40, + height: 40, + fit: 'contain', + ); + + self::assertSame('image/svg+xml', $response->getHeaderLine('content-type')); + self::assertStringContainsString( + 'UnableTo', + $response->getHeaderLine('x-thumbnail-exception'), + ); + } + + private function mediaFilesystem(): FilesystemOperator + { + $root = dirname(__DIR__, 2) . '/data/media'; + + return new Filesystem(new LocalFilesystemAdapter($root)); + } } diff --git a/tests/data/media/safe.svg b/tests/data/media/safe.svg new file mode 100644 index 0000000000..b802fe2e26 --- /dev/null +++ b/tests/data/media/safe.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="2" height="2"><rect width="2" height="2" fill="#0f0"/></svg> diff --git a/tests/data/media/unsafe.svg b/tests/data/media/unsafe.svg new file mode 100644 index 0000000000..f1e1a68567 --- /dev/null +++ b/tests/data/media/unsafe.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="2" height="2"><script>alert(1)</script></svg> |
