3
0
mirror of https://github.com/snipe/snipe-it.git synced 2026-08-18 11:15:42 +00:00
Files
snipe-it/tests/Unit/Rules/AllowedUploadExtensionTest.php
snipe 5478c671ab Fixes #10387, #12460, and #19389 - better validation for text files
Files whose sniff yields nothing usable (empty, octet-stream, INI-shaped) now pass.
2026-07-28 21:16:39 +01:00

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());
}
}