summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorGreg Roach <greg@subaqua.co.uk>2026-04-27 22:02:50 +0100
committerGreg Roach <greg@subaqua.co.uk>2026-04-27 22:33:02 +0100
commitd32e3cfb19891929a9c842586e92370438e27ae7 (patch)
tree22c34f5f4920f0cae1f81a2b4dad075182ca0ed7 /tests
parent751c9906a0e1029bd558c4223ff05c74f5f9753a (diff)
downloadwebtrees-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.php133
-rw-r--r--tests/data/media/safe.svg1
-rw-r--r--tests/data/media/unsafe.svg1
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>