mirror of
https://github.com/snipe/snipe-it.git
synced 2026-08-18 11:15:42 +00:00
167 lines
5.6 KiB
PHP
167 lines
5.6 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Rules;
|
|
|
|
use App\Rules\AllowedUploadExtension;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use Tests\TestCase;
|
|
|
|
class AllowedUploadExtensionTest extends TestCase
|
|
{
|
|
private array $tempFiles = [];
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
foreach ($this->tempFiles as $path) {
|
|
@unlink($path);
|
|
}
|
|
|
|
parent::tearDown();
|
|
}
|
|
|
|
// Real UploadedFile pointed at a real temp file. UploadedFile::fake()
|
|
// bypasses finfo (its getMimeType() reads MimeType::from($name), which
|
|
// is extension-only), so it can't reproduce the sniff-vs-extension
|
|
// mismatch this rule exists to tolerate.
|
|
private function realUpload(string $clientName, string $content): UploadedFile
|
|
{
|
|
$path = tempnam(sys_get_temp_dir(), 'snipeit_rule_');
|
|
file_put_contents($path, $content);
|
|
$this->tempFiles[] = $path;
|
|
|
|
return new UploadedFile($path, $clientName, null, null, true);
|
|
}
|
|
|
|
private function passes(UploadedFile $file, array $extensions = ['txt', 'csv', 'jpg', 'pdf']): bool
|
|
{
|
|
return Validator::make(
|
|
['file' => $file],
|
|
['file' => [new AllowedUploadExtension($extensions)]],
|
|
)->passes();
|
|
}
|
|
|
|
#[Test]
|
|
public function accepts_plain_text_file_with_matching_extension(): void
|
|
{
|
|
$this->assertTrue($this->passes(
|
|
$this->realUpload('notes.txt', "hello world\nmore text\n"),
|
|
));
|
|
}
|
|
|
|
// Issue #12460: empty text upload. finfo returns application/x-empty,
|
|
// which does not reverse-map to any extension. Laravel's `mimes:txt`
|
|
// would reject; this rule accepts the client-supplied extension.
|
|
#[Test]
|
|
public function accepts_empty_txt_file(): void
|
|
{
|
|
$this->assertTrue($this->passes(
|
|
$this->realUpload('empty.txt', ''),
|
|
));
|
|
}
|
|
|
|
// Issue #12460 (TechWilk repro): plain text whose first byte is `;`
|
|
// and whose fields are tab-separated matches libmagic's INI heuristic.
|
|
// finfo returns application/x-wine-extension-ini; guessExtension()
|
|
// returns null. The rule should still accept it because the client
|
|
// extension is on the allowlist.
|
|
#[Test]
|
|
public function accepts_txt_file_that_libmagic_misidentifies_as_ini(): void
|
|
{
|
|
$this->assertTrue($this->passes(
|
|
$this->realUpload('sample.txt', ";Bob[A]\tSmith[B]\r\n50\t0.8"),
|
|
));
|
|
}
|
|
|
|
// Issue #10387: CSVs whose sniffed MIME is unhelpful (Windows/IIS
|
|
// finfo commonly returns application/octet-stream) or that trip a
|
|
// non-CSV magic signature. The UploadFileRequest path should still
|
|
// accept those when the extension is on the allowlist.
|
|
#[Test]
|
|
public function accepts_csv_when_content_sniff_yields_octet_stream(): void
|
|
{
|
|
$this->assertTrue($this->passes(
|
|
$this->realUpload('inventory.csv', "\x00\x01\x02random,binary,bytes\n"),
|
|
['csv', 'txt'],
|
|
));
|
|
}
|
|
|
|
// The extension-check backstop still catches the classic mislabel:
|
|
// a PNG (well-known magic bytes) renamed to .txt. Client extension
|
|
// passes, but guessExtension() returns 'png' and 'png' is not in the
|
|
// allowlist, so the rule rejects.
|
|
#[Test]
|
|
public function rejects_binary_file_whose_sniff_disagrees_with_allowlist(): void
|
|
{
|
|
$png = base64_decode(
|
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
|
|
);
|
|
|
|
$this->assertFalse($this->passes(
|
|
$this->realUpload('sneaky.txt', $png),
|
|
['txt', 'csv'], // png deliberately absent
|
|
));
|
|
}
|
|
|
|
#[Test]
|
|
public function rejects_extension_not_on_allowlist(): void
|
|
{
|
|
$this->assertFalse($this->passes(
|
|
$this->realUpload('installer.exe', "MZ\x90\x00"),
|
|
));
|
|
}
|
|
|
|
#[Test]
|
|
public function rejects_php_executable_extension_even_if_allowlisted(): void
|
|
{
|
|
// Even a maliciously-permissive caller can't punch a PHP file
|
|
// through. This mirrors Laravel's own shouldBlockPhpUpload guard.
|
|
$this->assertFalse($this->passes(
|
|
$this->realUpload('shell.php', "<?php system(\$_GET['c']);"),
|
|
['txt', 'php'],
|
|
));
|
|
}
|
|
|
|
// The webshell case: PHP source dressed up as an image. finfo sniffs
|
|
// to text/x-php, which has no reverse map in Symfony's guesser (so the
|
|
// sniff crosscheck can't catch it), and my "uninformative sniff"
|
|
// fallback would otherwise let it through. The explicit executable-MIME
|
|
// belt catches it.
|
|
#[Test]
|
|
public function rejects_php_content_disguised_as_an_image(): void
|
|
{
|
|
$this->assertFalse($this->passes(
|
|
$this->realUpload('shell.jpg', "<?php system(\$_GET['c']); ?>"),
|
|
));
|
|
}
|
|
|
|
#[Test]
|
|
public function rejects_php_content_disguised_as_text(): void
|
|
{
|
|
$this->assertFalse($this->passes(
|
|
$this->realUpload('notes.txt', "<?php echo 'hi'; ?>"),
|
|
));
|
|
}
|
|
|
|
// Shebang scripts stay allowed. Snipe-IT does not execute uploads and
|
|
// legitimate script snippets end up in .txt support-ticket attachments
|
|
// often enough that rejecting them is user-hostile.
|
|
#[Test]
|
|
public function accepts_shell_script_content_in_txt(): void
|
|
{
|
|
$this->assertTrue($this->passes(
|
|
$this->realUpload('snippet.txt', "#!/bin/bash\necho hi\n"),
|
|
));
|
|
}
|
|
|
|
#[Test]
|
|
public function rejects_non_file_values(): void
|
|
{
|
|
$this->assertFalse(Validator::make(
|
|
['file' => 'not-a-file'],
|
|
['file' => [new AllowedUploadExtension(['txt'])]],
|
|
)->passes());
|
|
}
|
|
}
|