Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
715
tests/Framework/Http/Parser/FileUploadParserTest.php
Normal file
715
tests/Framework/Http/Parser/FileUploadParserTest.php
Normal file
@@ -0,0 +1,715 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Framework\Http\Parser;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Byte;
|
||||
use App\Framework\Http\Parser\Exception\ParserSecurityException;
|
||||
use App\Framework\Http\Parser\FileUploadParser;
|
||||
use App\Framework\Http\Parser\ParserConfig;
|
||||
use App\Framework\Http\UploadError;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class FileUploadParserTest extends TestCase
|
||||
{
|
||||
private FileUploadParser $parser;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->parser = new FileUploadParser();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
// Clean up any temporary files created during tests
|
||||
$tempDir = sys_get_temp_dir();
|
||||
$files = glob($tempDir . '/upload_*');
|
||||
foreach ($files as $file) {
|
||||
if (is_file($file)) {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testParseMultipartSingleFile(): void
|
||||
{
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename=\"test.txt\"\r\n" .
|
||||
"Content-Type: text/plain\r\n" .
|
||||
"\r\n" .
|
||||
"Hello World!\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $this->parser->parseMultipart($body, $boundary);
|
||||
|
||||
$this->assertCount(1, $result->all());
|
||||
|
||||
$file = $result->get('upload');
|
||||
$this->assertNotNull($file);
|
||||
$this->assertSame('test.txt', $file->name);
|
||||
$this->assertSame('text/plain', $file->type);
|
||||
$this->assertSame(12, $file->size); // "Hello World!" length
|
||||
$this->assertSame(UploadError::OK, $file->error);
|
||||
$this->assertTrue(file_exists($file->tmpName));
|
||||
$this->assertSame('Hello World!', file_get_contents($file->tmpName));
|
||||
}
|
||||
|
||||
public function testParseMultipartMultipleFiles(): void
|
||||
{
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"file1\"; filename=\"test1.txt\"\r\n" .
|
||||
"Content-Type: text/plain\r\n" .
|
||||
"\r\n" .
|
||||
"Content 1\r\n" .
|
||||
"------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"file2\"; filename=\"test2.txt\"\r\n" .
|
||||
"Content-Type: text/plain\r\n" .
|
||||
"\r\n" .
|
||||
"Content 2\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $this->parser->parseMultipart($body, $boundary);
|
||||
|
||||
$this->assertCount(2, $result->all());
|
||||
|
||||
$file1 = $result->get('file1');
|
||||
$file2 = $result->get('file2');
|
||||
|
||||
$this->assertNotNull($file1);
|
||||
$this->assertNotNull($file2);
|
||||
|
||||
$this->assertSame('test1.txt', $file1->name);
|
||||
$this->assertSame('test2.txt', $file2->name);
|
||||
$this->assertSame('Content 1', file_get_contents($file1->tmpName));
|
||||
$this->assertSame('Content 2', file_get_contents($file2->tmpName));
|
||||
}
|
||||
|
||||
public function testParseMultipartFileArray(): void
|
||||
{
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"files[]\"; filename=\"file1.txt\"\r\n" .
|
||||
"Content-Type: text/plain\r\n" .
|
||||
"\r\n" .
|
||||
"File 1\r\n" .
|
||||
"------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"files[]\"; filename=\"file2.txt\"\r\n" .
|
||||
"Content-Type: text/plain\r\n" .
|
||||
"\r\n" .
|
||||
"File 2\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $this->parser->parseMultipart($body, $boundary);
|
||||
|
||||
$files = $result->get('files');
|
||||
$this->assertIsArray($files);
|
||||
$this->assertCount(2, $files);
|
||||
|
||||
$this->assertSame('file1.txt', $files[0]->name);
|
||||
$this->assertSame('file2.txt', $files[1]->name);
|
||||
$this->assertSame('File 1', file_get_contents($files[0]->tmpName));
|
||||
$this->assertSame('File 2', file_get_contents($files[1]->tmpName));
|
||||
}
|
||||
|
||||
public function testParseMultipartNestedFileArray(): void
|
||||
{
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"docs[legal][]\"; filename=\"contract.pdf\"\r\n" .
|
||||
"Content-Type: application/pdf\r\n" .
|
||||
"\r\n" .
|
||||
"%PDF-1.4 PDF content here\r\n" . // Add PDF signature to match MIME type
|
||||
"------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"docs[images][0]\"; filename=\"logo.png\"\r\n" .
|
||||
"Content-Type: image/png\r\n" .
|
||||
"\r\n" .
|
||||
"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A" . "PNG content here\r\n" . // Add PNG signature
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $this->parser->parseMultipart($body, $boundary);
|
||||
|
||||
$docs = $result->get('docs');
|
||||
$this->assertIsArray($docs);
|
||||
$this->assertArrayHasKey('legal', $docs);
|
||||
$this->assertArrayHasKey('images', $docs);
|
||||
|
||||
$legalFiles = $docs['legal'];
|
||||
$this->assertIsArray($legalFiles);
|
||||
$this->assertCount(1, $legalFiles);
|
||||
$this->assertSame('contract.pdf', $legalFiles[0]->name);
|
||||
$this->assertSame('application/pdf', $legalFiles[0]->type);
|
||||
|
||||
$imageFiles = $docs['images'];
|
||||
$this->assertIsArray($imageFiles);
|
||||
$this->assertSame('logo.png', $imageFiles['0']->name);
|
||||
$this->assertSame('image/png', $imageFiles['0']->type);
|
||||
}
|
||||
|
||||
public function testParseMultipartWithoutFilename(): void
|
||||
{
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"data\"\r\n" .
|
||||
"Content-Type: text/plain\r\n" .
|
||||
"\r\n" .
|
||||
"Just regular form data\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $this->parser->parseMultipart($body, $boundary);
|
||||
|
||||
// Should not create any files for parts without filename
|
||||
$this->assertCount(0, $result->all());
|
||||
}
|
||||
|
||||
public function testParseMultipartDefaultContentType(): void
|
||||
{
|
||||
// Use a parser with relaxed security for this test
|
||||
$config = new ParserConfig(validateFileExtensions: false, scanForMaliciousContent: false);
|
||||
$parser = new FileUploadParser($config);
|
||||
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename=\"binary.dat\"\r\n" .
|
||||
"\r\n" .
|
||||
"Binary data here\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $parser->parseMultipart($body, $boundary);
|
||||
|
||||
$file = $result->get('upload');
|
||||
$this->assertNotNull($file);
|
||||
$this->assertSame('application/octet-stream', $file->type);
|
||||
}
|
||||
|
||||
public function testParseMultipartRfc2231ExtendedFilename(): void
|
||||
{
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename*=UTF-8''caf%C3%A9.txt\r\n" .
|
||||
"Content-Type: text/plain\r\n" .
|
||||
"\r\n" .
|
||||
"Content\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $this->parser->parseMultipart($body, $boundary);
|
||||
|
||||
$file = $result->get('upload');
|
||||
$this->assertNotNull($file);
|
||||
$this->assertSame('café.txt', $file->name); // Should be properly decoded
|
||||
}
|
||||
|
||||
public function testParseMultipartRfc2231InvalidFormat(): void
|
||||
{
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename*=invalid_format\r\n" .
|
||||
"Content-Type: text/plain\r\n" .
|
||||
"\r\n" .
|
||||
"Content\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $this->parser->parseMultipart($body, $boundary);
|
||||
|
||||
$file = $result->get('upload');
|
||||
$this->assertNotNull($file);
|
||||
$this->assertSame('invalid_format', $file->name); // Should return as-is for invalid format
|
||||
}
|
||||
|
||||
public function testParseMultipartEmptyFile(): void
|
||||
{
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename=\"empty.txt\"\r\n" .
|
||||
"Content-Type: text/plain\r\n" .
|
||||
"\r\n" .
|
||||
"\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $this->parser->parseMultipart($body, $boundary);
|
||||
|
||||
$file = $result->get('upload');
|
||||
$this->assertNotNull($file);
|
||||
$this->assertSame('empty.txt', $file->name);
|
||||
$this->assertSame(0, $file->size);
|
||||
$this->assertSame('', file_get_contents($file->tmpName));
|
||||
}
|
||||
|
||||
public function testParseMultipartMalformedParts(): void
|
||||
{
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Malformed part without proper headers\r\n" .
|
||||
"------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"valid\"; filename=\"test.txt\"\r\n" .
|
||||
"\r\n" .
|
||||
"Valid content\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $this->parser->parseMultipart($body, $boundary);
|
||||
|
||||
// Should ignore malformed parts and process valid ones
|
||||
$this->assertCount(1, $result->all());
|
||||
$file = $result->get('valid');
|
||||
$this->assertNotNull($file);
|
||||
$this->assertSame('test.txt', $file->name);
|
||||
}
|
||||
|
||||
public function testParseMultipartMissingName(): void
|
||||
{
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; filename=\"test.txt\"\r\n" .
|
||||
"Content-Type: text/plain\r\n" .
|
||||
"\r\n" .
|
||||
"Content\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $this->parser->parseMultipart($body, $boundary);
|
||||
|
||||
// Should ignore parts without name attribute
|
||||
$this->assertCount(0, $result->all());
|
||||
}
|
||||
|
||||
public function testParseMultipartQuotedValues(): void
|
||||
{
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename=\"my file.txt\"\r\n" .
|
||||
"Content-Type: text/plain\r\n" .
|
||||
"\r\n" .
|
||||
"Content with spaces in filename\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $this->parser->parseMultipart($body, $boundary);
|
||||
|
||||
$file = $result->get('upload');
|
||||
$this->assertNotNull($file);
|
||||
$this->assertSame('my file.txt', $file->name);
|
||||
}
|
||||
|
||||
public function testParseMultipartLargeFile(): void
|
||||
{
|
||||
$boundary = '----FormBoundary123';
|
||||
$largeContent = str_repeat('A', 10000); // 10KB of 'A's
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"large\"; filename=\"large.txt\"\r\n" .
|
||||
"Content-Type: text/plain\r\n" .
|
||||
"\r\n" .
|
||||
$largeContent . "\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $this->parser->parseMultipart($body, $boundary);
|
||||
|
||||
$file = $result->get('large');
|
||||
$this->assertNotNull($file);
|
||||
$this->assertSame(10000, $file->size);
|
||||
$this->assertSame($largeContent, file_get_contents($file->tmpName));
|
||||
}
|
||||
|
||||
public function testTemporaryFileCleanup(): void
|
||||
{
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename=\"test.txt\"\r\n" .
|
||||
"Content-Type: text/plain\r\n" .
|
||||
"\r\n" .
|
||||
"Test content\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $this->parser->parseMultipart($body, $boundary);
|
||||
$file = $result->get('upload');
|
||||
$tmpPath = $file->tmpName;
|
||||
|
||||
$this->assertTrue(file_exists($tmpPath));
|
||||
|
||||
// Simulate script end - the shutdown function should clean up
|
||||
// We can't easily test this automatically, but the file path is registered
|
||||
// for cleanup in the shutdown function
|
||||
$this->assertStringStartsWith(sys_get_temp_dir() . '/upload_', $tmpPath);
|
||||
}
|
||||
|
||||
// Security Tests
|
||||
|
||||
public function testBoundaryTooLong(): void
|
||||
{
|
||||
$config = new ParserConfig(maxBoundaryLength: 10);
|
||||
$parser = new FileUploadParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('Multipart boundary too long');
|
||||
|
||||
$longBoundary = str_repeat('a', 15);
|
||||
$body = "--{$longBoundary}\r\nContent-Disposition: form-data; name=\"test\"; filename=\"test.txt\"\r\n\r\nvalue\r\n--{$longBoundary}--\r\n";
|
||||
$parser->parseMultipart($body, $longBoundary);
|
||||
}
|
||||
|
||||
public function testFileCountExceeded(): void
|
||||
{
|
||||
$config = new ParserConfig(maxFileCount: 2);
|
||||
$parser = new FileUploadParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('File count exceeded');
|
||||
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"file1\"; filename=\"test1.txt\"\r\n\r\nContent 1\r\n" .
|
||||
"------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"file2\"; filename=\"test2.txt\"\r\n\r\nContent 2\r\n" .
|
||||
"------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"file3\"; filename=\"test3.txt\"\r\n\r\nContent 3\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$parser->parseMultipart($body, $boundary);
|
||||
}
|
||||
|
||||
public function testFileSizeExceeded(): void
|
||||
{
|
||||
$config = new ParserConfig(maxFileSize: new Byte(50));
|
||||
$parser = new FileUploadParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('File size exceeded');
|
||||
|
||||
$boundary = '----FormBoundary123';
|
||||
$largeContent = str_repeat('a', 100); // Exceeds 50 byte limit
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename=\"large.txt\"\r\n\r\n" .
|
||||
"{$largeContent}\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$parser->parseMultipart($body, $boundary);
|
||||
}
|
||||
|
||||
public function testTotalUploadSizeExceeded(): void
|
||||
{
|
||||
$config = new ParserConfig(maxTotalUploadSize: new Byte(100));
|
||||
$parser = new FileUploadParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('Total upload size exceeded');
|
||||
|
||||
$boundary = '----FormBoundary123';
|
||||
$content1 = str_repeat('a', 60); // 60 bytes
|
||||
$content2 = str_repeat('b', 50); // 50 bytes - total 110 bytes > 100 limit
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"file1\"; filename=\"file1.txt\"\r\n\r\n" .
|
||||
"{$content1}\r\n" .
|
||||
"------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"file2\"; filename=\"file2.txt\"\r\n\r\n" .
|
||||
"{$content2}\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$parser->parseMultipart($body, $boundary);
|
||||
}
|
||||
|
||||
public function testBlockedFileExtension(): void
|
||||
{
|
||||
$config = new ParserConfig(blockedFileExtensions: ['php', 'exe']);
|
||||
$parser = new FileUploadParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('File extension blocked');
|
||||
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename=\"malicious.php\"\r\n\r\n" .
|
||||
"<?php echo 'hack'; ?>\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$parser->parseMultipart($body, $boundary);
|
||||
}
|
||||
|
||||
public function testFileExtensionNotAllowed(): void
|
||||
{
|
||||
$config = new ParserConfig(allowedFileExtensions: ['txt', 'jpg']);
|
||||
$parser = new FileUploadParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('File extension not allowed');
|
||||
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename=\"document.pdf\"\r\n\r\n" .
|
||||
"PDF content\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$parser->parseMultipart($body, $boundary);
|
||||
}
|
||||
|
||||
public function testMaliciousExecutableContent(): void
|
||||
{
|
||||
$config = new ParserConfig(
|
||||
scanForMaliciousContent: true,
|
||||
allowedFileExtensions: ['exe', 'txt'], // Allow exe to test content validation
|
||||
blockedFileExtensions: [] // Remove blocked extensions to test content
|
||||
);
|
||||
$parser = new FileUploadParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('Executable content detected');
|
||||
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename=\"malware.exe\"\r\n\r\n" .
|
||||
"\x4D\x5A" . str_repeat("x", 100) . "\r\n" . // PE executable signature
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$parser->parseMultipart($body, $boundary);
|
||||
}
|
||||
|
||||
public function testMaliciousPhpContent(): void
|
||||
{
|
||||
$config = new ParserConfig(
|
||||
scanForMaliciousContent: true,
|
||||
allowedFileExtensions: ['txt', 'php'], // Allow txt to test content validation
|
||||
blockedFileExtensions: [] // Remove blocked extensions to test content
|
||||
);
|
||||
$parser = new FileUploadParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('PHP code detected');
|
||||
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename=\"shell.txt\"\r\n\r\n" .
|
||||
"<?php system(\$_GET['cmd']); ?>\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$parser->parseMultipart($body, $boundary);
|
||||
}
|
||||
|
||||
public function testMaliciousScriptContent(): void
|
||||
{
|
||||
$config = new ParserConfig(
|
||||
scanForMaliciousContent: true,
|
||||
allowedFileExtensions: ['html', 'txt'], // Allow html to test content validation
|
||||
blockedFileExtensions: [] // Remove blocked extensions to test content
|
||||
);
|
||||
$parser = new FileUploadParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('Suspicious script content detected');
|
||||
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename=\"xss.html\"\r\n\r\n" .
|
||||
"<script>alert('xss')</script>\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$parser->parseMultipart($body, $boundary);
|
||||
}
|
||||
|
||||
public function testMaliciousEvalContent(): void
|
||||
{
|
||||
$config = new ParserConfig(
|
||||
scanForMaliciousContent: true,
|
||||
allowedFileExtensions: ['js', 'txt'], // Allow js to test content validation
|
||||
blockedFileExtensions: [] // Remove blocked extensions to test content
|
||||
);
|
||||
$parser = new FileUploadParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('Suspicious script content detected');
|
||||
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename=\"evil.js\"\r\n\r\n" .
|
||||
"eval(atob('YWxlcnQoJ2hhY2snKQ=='))\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$parser->parseMultipart($body, $boundary);
|
||||
}
|
||||
|
||||
public function testMimeTypeMismatch(): void
|
||||
{
|
||||
$config = new ParserConfig(scanForMaliciousContent: true, strictMimeTypeValidation: true);
|
||||
$parser = new FileUploadParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('MIME type mismatch');
|
||||
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename=\"fake.jpg\"\r\n" .
|
||||
"Content-Type: image/jpeg\r\n\r\n" .
|
||||
"%PDF-1.4 This is actually a PDF file\r\n" . // PDF signature but claiming to be JPEG
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$parser->parseMultipart($body, $boundary);
|
||||
}
|
||||
|
||||
public function testShellScriptDetection(): void
|
||||
{
|
||||
$config = new ParserConfig(scanForMaliciousContent: true);
|
||||
$parser = new FileUploadParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('Executable content detected');
|
||||
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename=\"script.txt\"\r\n\r\n" .
|
||||
"#!/bin/bash\nrm -rf /\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$parser->parseMultipart($body, $boundary);
|
||||
}
|
||||
|
||||
public function testSystemCallDetection(): void
|
||||
{
|
||||
$config = new ParserConfig(scanForMaliciousContent: true);
|
||||
$parser = new FileUploadParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('Suspicious script content detected');
|
||||
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename=\"backdoor.txt\"\r\n\r\n" .
|
||||
"system('cat /etc/passwd')\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$parser->parseMultipart($body, $boundary);
|
||||
}
|
||||
|
||||
// Security Configuration Tests
|
||||
|
||||
public function testSecurityDisabled(): void
|
||||
{
|
||||
$config = new ParserConfig(
|
||||
scanForMaliciousContent: false,
|
||||
validateFileExtensions: false,
|
||||
allowedFileExtensions: [], // Empty to allow all when validation disabled
|
||||
blockedFileExtensions: [], // Empty to not block anything when validation disabled
|
||||
maxFileCount: 1000,
|
||||
maxFileSize: new Byte(10 * 1024 * 1024),
|
||||
maxTotalUploadSize: new Byte(100 * 1024 * 1024)
|
||||
);
|
||||
$parser = new FileUploadParser($config);
|
||||
|
||||
// Should not throw exception when security is disabled
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename=\"malicious.php\"\r\n\r\n" .
|
||||
"<?php system(\$_GET['cmd']); ?>\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $parser->parseMultipart($body, $boundary);
|
||||
$file = $result->get('upload');
|
||||
$this->assertNotNull($file);
|
||||
$this->assertSame('malicious.php', $file->name);
|
||||
}
|
||||
|
||||
public function testWithinSecurityLimits(): void
|
||||
{
|
||||
$config = new ParserConfig(
|
||||
maxFileCount: 5,
|
||||
maxFileSize: new Byte(1024),
|
||||
maxTotalUploadSize: new Byte(5 * 1024),
|
||||
allowedFileExtensions: ['txt', 'jpg', 'png'],
|
||||
scanForMaliciousContent: true
|
||||
);
|
||||
$parser = new FileUploadParser($config);
|
||||
|
||||
// Should work fine within limits
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"file1\"; filename=\"test1.txt\"\r\n\r\n" .
|
||||
"Safe content 1\r\n" .
|
||||
"------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"file2\"; filename=\"image.jpg\"\r\n\r\n" .
|
||||
"\xFF\xD8\xFF" . str_repeat('x', 10) . "\r\n" . // Valid JPEG signature
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $parser->parseMultipart($body, $boundary);
|
||||
$this->assertCount(2, $result->all());
|
||||
|
||||
$file1 = $result->get('file1');
|
||||
$file2 = $result->get('file2');
|
||||
$this->assertNotNull($file1);
|
||||
$this->assertNotNull($file2);
|
||||
$this->assertSame('test1.txt', $file1->name);
|
||||
$this->assertSame('image.jpg', $file2->name);
|
||||
}
|
||||
|
||||
public function testAllowedExtensionsEmptyList(): void
|
||||
{
|
||||
$config = new ParserConfig(
|
||||
allowedFileExtensions: [], // Empty list should allow all (except blocked)
|
||||
blockedFileExtensions: ['exe'],
|
||||
scanForMaliciousContent: false
|
||||
);
|
||||
$parser = new FileUploadParser($config);
|
||||
|
||||
// Should allow PDF when allowed list is empty
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename=\"document.pdf\"\r\n\r\n" .
|
||||
"PDF content\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $parser->parseMultipart($body, $boundary);
|
||||
$file = $result->get('upload');
|
||||
$this->assertNotNull($file);
|
||||
$this->assertSame('document.pdf', $file->name);
|
||||
}
|
||||
|
||||
public function testCompatibleMimeTypes(): void
|
||||
{
|
||||
$config = new ParserConfig(scanForMaliciousContent: true, strictMimeTypeValidation: true);
|
||||
$parser = new FileUploadParser($config);
|
||||
|
||||
// Should allow compatible MIME types (image/jpg vs image/jpeg)
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename=\"photo.jpg\"\r\n" .
|
||||
"Content-Type: image/jpg\r\n\r\n" .
|
||||
"\xFF\xD8\xFF" . str_repeat('x', 10) . "\r\n" . // Valid JPEG signature
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $parser->parseMultipart($body, $boundary);
|
||||
$file = $result->get('upload');
|
||||
$this->assertNotNull($file);
|
||||
$this->assertSame('photo.jpg', $file->name);
|
||||
}
|
||||
|
||||
public function testNoExtensionFile(): void
|
||||
{
|
||||
$config = new ParserConfig(validateFileExtensions: true, allowedFileExtensions: ['txt']);
|
||||
$parser = new FileUploadParser($config);
|
||||
|
||||
// Should allow files without extension (no validation performed)
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename=\"README\"\r\n\r\n" .
|
||||
"This is a README file\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $parser->parseMultipart($body, $boundary);
|
||||
$file = $result->get('upload');
|
||||
$this->assertNotNull($file);
|
||||
$this->assertSame('README', $file->name);
|
||||
}
|
||||
|
||||
public function testEmptyFilename(): void
|
||||
{
|
||||
$config = new ParserConfig(validateFileExtensions: true, allowedFileExtensions: ['txt']);
|
||||
$parser = new FileUploadParser($config);
|
||||
|
||||
// Should allow empty filename (no validation performed)
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename=\"\"\r\n\r\n" .
|
||||
"Content without filename\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $parser->parseMultipart($body, $boundary);
|
||||
$file = $result->get('upload');
|
||||
$this->assertNotNull($file);
|
||||
$this->assertSame('', $file->name);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user