mirror of
https://github.com/snipe/snipe-it.git
synced 2026-08-18 11:15:42 +00:00
117 lines
4.4 KiB
PHP
117 lines
4.4 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\FileUploads;
|
|
|
|
use App\Http\Requests\UploadFileRequest;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Regression guard for SVG upload sanitization.
|
|
*
|
|
* `UploadFileRequest::handleFile` detects `image/svg+xml` via
|
|
* server-side finfo and pipes the content through
|
|
* `enshrined/svg-sanitize` before writing to disk. That's what stops
|
|
* a stored-XSS via inline SVG rendering (the download endpoint
|
|
* serves .svg with Content-Disposition: inline when the extension
|
|
* and detected MIME both match).
|
|
*
|
|
* External report against a June 2026 master snapshot suggested the
|
|
* inline-SVG XSS was still exploitable via <script> or onload=""
|
|
* payloads. It isn't. The upload-time sanitizer strips both shapes
|
|
* before the file reaches disk, but the report was accurate about
|
|
* the inline-serving path, so this test locks in the upload-time
|
|
* sanitization so a future refactor can't quietly remove the SVG
|
|
* branch and reintroduce the exploit chain.
|
|
*/
|
|
class SvgUploadSanitizationTest extends TestCase
|
|
{
|
|
private array $tempFiles = [];
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
Storage::fake();
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
foreach ($this->tempFiles as $path) {
|
|
@unlink($path);
|
|
}
|
|
|
|
parent::tearDown();
|
|
}
|
|
|
|
private function realUpload(string $clientName, string $content): UploadedFile
|
|
{
|
|
// A real temp file is required for finfo to sniff the MIME
|
|
// correctly. UploadedFile::fake() derives its mime from the
|
|
// filename, which would trivially "pass" the SVG detection
|
|
// without exercising the real sanitize path.
|
|
$path = tempnam(sys_get_temp_dir(), 'snipeit_svg_');
|
|
file_put_contents($path, $content);
|
|
$this->tempFiles[] = $path;
|
|
|
|
return new UploadedFile($path, $clientName, null, null, true);
|
|
}
|
|
|
|
public function test_svg_upload_strips_inline_script_element(): void
|
|
{
|
|
$malicious = <<<'SVG'
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
|
|
<script type="text/javascript">alert(document.domain)</script>
|
|
<rect width="100" height="100" fill="red"/>
|
|
</svg>
|
|
SVG;
|
|
|
|
$upload = $this->realUpload('malicious.svg', $malicious);
|
|
|
|
$storedName = (new UploadFileRequest)->handleFile('private_uploads/assets/', 'asset-1', $upload);
|
|
$storedContents = Storage::get('private_uploads/assets/'.$storedName);
|
|
|
|
$this->assertStringNotContainsString('<script', $storedContents, 'Sanitizer must strip <script> elements from uploaded SVGs.');
|
|
$this->assertStringNotContainsString('alert(', $storedContents, 'Sanitizer must strip script bodies from uploaded SVGs.');
|
|
// Legitimate SVG content should survive so the file is still
|
|
// a usable image after sanitization.
|
|
$this->assertStringContainsString('<rect', $storedContents);
|
|
}
|
|
|
|
public function test_svg_upload_strips_onload_event_handler(): void
|
|
{
|
|
$malicious = <<<'SVG'
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" onload="alert(document.domain)">
|
|
<rect width="100" height="100" fill="blue"/>
|
|
</svg>
|
|
SVG;
|
|
|
|
$upload = $this->realUpload('onload.svg', $malicious);
|
|
|
|
$storedName = (new UploadFileRequest)->handleFile('private_uploads/assets/', 'asset-1', $upload);
|
|
$storedContents = Storage::get('private_uploads/assets/'.$storedName);
|
|
|
|
$this->assertStringNotContainsString('onload', $storedContents, 'Sanitizer must strip on* event handlers from uploaded SVGs.');
|
|
$this->assertStringNotContainsString('alert(', $storedContents);
|
|
$this->assertStringContainsString('<rect', $storedContents);
|
|
}
|
|
|
|
public function test_clean_svg_upload_passes_through_unchanged_content(): void
|
|
{
|
|
// Non-regression: legitimate SVGs must still be usable.
|
|
$clean = <<<'SVG'
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50">
|
|
<circle cx="25" cy="25" r="20" fill="green"/>
|
|
</svg>
|
|
SVG;
|
|
|
|
$upload = $this->realUpload('logo.svg', $clean);
|
|
|
|
$storedName = (new UploadFileRequest)->handleFile('private_uploads/assets/', 'asset-1', $upload);
|
|
$storedContents = Storage::get('private_uploads/assets/'.$storedName);
|
|
|
|
$this->assertStringContainsString('<circle', $storedContents);
|
|
$this->assertStringContainsString('fill="green"', $storedContents);
|
|
}
|
|
}
|