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:
535
tests/Framework/Http/Parser/FormDataParserTest.php
Normal file
535
tests/Framework/Http/Parser/FormDataParserTest.php
Normal file
@@ -0,0 +1,535 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Framework\Http\Parser;
|
||||
|
||||
use App\Framework\Cache\Compression\NullCompression;
|
||||
use App\Framework\Cache\CompressionCacheDecorator;
|
||||
use App\Framework\Cache\Driver\InMemoryCache;
|
||||
use App\Framework\Cache\GeneralCache;
|
||||
use App\Framework\Cache\Serializer\PhpSerializer;
|
||||
use App\Framework\Core\ValueObjects\Byte;
|
||||
use App\Framework\Http\Parser\Exception\ParserSecurityException;
|
||||
use App\Framework\Http\Parser\FormDataParser;
|
||||
use App\Framework\Http\Parser\ParserCache;
|
||||
use App\Framework\Http\Parser\ParserConfig;
|
||||
use App\Framework\Http\Parser\QueryStringParser;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class FormDataParserTest extends TestCase
|
||||
{
|
||||
private FormDataParser $parser;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->parser = $this->createFormDataParser();
|
||||
}
|
||||
|
||||
private function createFormDataParser(?ParserConfig $config = null): FormDataParser
|
||||
{
|
||||
// Create parser cache with proper serialization
|
||||
$baseCache = new GeneralCache(new InMemoryCache(), new \App\Framework\Serializer\Php\PhpSerializer());
|
||||
$compressionCache = new CompressionCacheDecorator(
|
||||
$baseCache,
|
||||
new NullCompression(),
|
||||
new PhpSerializer()
|
||||
);
|
||||
$cache = new ParserCache($compressionCache);
|
||||
$config = $config ?? new ParserConfig();
|
||||
|
||||
// FormDataParser needs QueryStringParser as second parameter
|
||||
$queryParser = new QueryStringParser($config, $cache);
|
||||
|
||||
return new FormDataParser($config, $queryParser);
|
||||
}
|
||||
|
||||
public function testParseEmptyBody(): void
|
||||
{
|
||||
$result = $this->parser->parse('application/x-www-form-urlencoded', '');
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testParseUrlEncodedSimple(): void
|
||||
{
|
||||
$body = 'name=John&email=john@example.com&age=30';
|
||||
$result = $this->parser->parse('application/x-www-form-urlencoded', $body);
|
||||
|
||||
$this->assertSame([
|
||||
'name' => 'John',
|
||||
'email' => 'john@example.com',
|
||||
'age' => '30',
|
||||
], $result);
|
||||
}
|
||||
|
||||
public function testParseUrlEncodedWithSpaces(): void
|
||||
{
|
||||
$body = 'message=Hello+World&city=New+York';
|
||||
$result = $this->parser->parse('application/x-www-form-urlencoded', $body);
|
||||
|
||||
$this->assertSame([
|
||||
'message' => 'Hello World',
|
||||
'city' => 'New York',
|
||||
], $result);
|
||||
}
|
||||
|
||||
public function testParseUrlEncodedWithSpecialChars(): void
|
||||
{
|
||||
$body = 'email=test%40example.com&message=Hello%21+How%3F';
|
||||
$result = $this->parser->parse('application/x-www-form-urlencoded', $body);
|
||||
|
||||
$this->assertSame([
|
||||
'email' => 'test@example.com',
|
||||
'message' => 'Hello! How?',
|
||||
], $result);
|
||||
}
|
||||
|
||||
public function testParseUrlEncodedArrays(): void
|
||||
{
|
||||
$body = 'tags[]=php&tags[]=web&user[name]=John&user[age]=30';
|
||||
$result = $this->parser->parse('application/x-www-form-urlencoded', $body);
|
||||
|
||||
$this->assertSame([
|
||||
'tags' => ['php', 'web'],
|
||||
'user' => [
|
||||
'name' => 'John',
|
||||
'age' => '30',
|
||||
],
|
||||
], $result);
|
||||
}
|
||||
|
||||
public function testParseUrlEncodedCaseInsensitiveContentType(): void
|
||||
{
|
||||
$body = 'name=John';
|
||||
$result = $this->parser->parse('APPLICATION/X-WWW-FORM-URLENCODED', $body);
|
||||
|
||||
$this->assertSame(['name' => 'John'], $result);
|
||||
}
|
||||
|
||||
public function testParseUrlEncodedWithCharset(): void
|
||||
{
|
||||
$body = 'name=John&message=Hello';
|
||||
$result = $this->parser->parse('application/x-www-form-urlencoded; charset=utf-8', $body);
|
||||
|
||||
$this->assertSame([
|
||||
'name' => 'John',
|
||||
'message' => 'Hello',
|
||||
], $result);
|
||||
}
|
||||
|
||||
public function testParseMultipartSimpleFields(): void
|
||||
{
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"username\"\r\n" .
|
||||
"\r\n" .
|
||||
"johndoe\r\n" .
|
||||
"------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"email\"\r\n" .
|
||||
"\r\n" .
|
||||
"john@example.com\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $this->parser->parse('multipart/form-data; boundary=----FormBoundary123', $body);
|
||||
|
||||
$this->assertSame([
|
||||
'username' => 'johndoe',
|
||||
'email' => 'john@example.com',
|
||||
], $result);
|
||||
}
|
||||
|
||||
public function testParseMultipartWithEmptyField(): void
|
||||
{
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"username\"\r\n" .
|
||||
"\r\n" .
|
||||
"johndoe\r\n" .
|
||||
"------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"optional\"\r\n" .
|
||||
"\r\n" .
|
||||
"\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $this->parser->parse('multipart/form-data; boundary=----FormBoundary123', $body);
|
||||
|
||||
$this->assertSame([
|
||||
'username' => 'johndoe',
|
||||
'optional' => '',
|
||||
], $result);
|
||||
}
|
||||
|
||||
public function testParseMultipartArrayFields(): void
|
||||
{
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"tags[]\"\r\n" .
|
||||
"\r\n" .
|
||||
"php\r\n" .
|
||||
"------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"tags[]\"\r\n" .
|
||||
"\r\n" .
|
||||
"web\r\n" .
|
||||
"------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"user[name]\"\r\n" .
|
||||
"\r\n" .
|
||||
"John\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $this->parser->parse('multipart/form-data; boundary=----FormBoundary123', $body);
|
||||
|
||||
$this->assertSame([
|
||||
'tags' => ['php', 'web'],
|
||||
'user' => ['name' => 'John'],
|
||||
], $result);
|
||||
}
|
||||
|
||||
public function testParseMultipartSkipsFileFields(): void
|
||||
{
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"username\"\r\n" .
|
||||
"\r\n" .
|
||||
"johndoe\r\n" .
|
||||
"------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"upload\"; filename=\"test.txt\"\r\n" .
|
||||
"Content-Type: text/plain\r\n" .
|
||||
"\r\n" .
|
||||
"File content here\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $this->parser->parse('multipart/form-data; boundary=----FormBoundary123', $body);
|
||||
|
||||
// Should only include regular form fields, not files
|
||||
$this->assertSame([
|
||||
'username' => 'johndoe',
|
||||
], $result);
|
||||
}
|
||||
|
||||
public function testParseMultipartWithContentType(): void
|
||||
{
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"data\"\r\n" .
|
||||
"Content-Type: application/json\r\n" .
|
||||
"\r\n" .
|
||||
"{\"key\":\"value\"}\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $this->parser->parse('multipart/form-data; boundary=----FormBoundary123', $body);
|
||||
|
||||
$this->assertSame([
|
||||
'data' => '{"key":"value"}',
|
||||
], $result);
|
||||
}
|
||||
|
||||
public function testParseMultipartMissingBoundaryThrowsException(): void
|
||||
{
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage('Missing boundary in multipart/form-data');
|
||||
|
||||
$this->parser->parse('multipart/form-data', 'some body');
|
||||
}
|
||||
|
||||
public function testParseUnsupportedContentType(): void
|
||||
{
|
||||
$result = $this->parser->parse('application/json', '{"key":"value"}');
|
||||
$this->assertSame([], $result);
|
||||
}
|
||||
|
||||
public function testParseMultipartWithQuotedBoundary(): void
|
||||
{
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"test\"\r\n" .
|
||||
"\r\n" .
|
||||
"value\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $this->parser->parse('multipart/form-data; boundary="----FormBoundary123"', $body);
|
||||
|
||||
$this->assertSame(['test' => 'value'], $result);
|
||||
}
|
||||
|
||||
public function testParseMultipartIgnoresMalformedParts(): void
|
||||
{
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"valid\"\r\n" .
|
||||
"\r\n" .
|
||||
"validvalue\r\n" .
|
||||
"------FormBoundary123\r\n" .
|
||||
"Malformed part without proper headers\r\n" .
|
||||
"------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"another\"\r\n" .
|
||||
"\r\n" .
|
||||
"anothervalue\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $this->parser->parse('multipart/form-data; boundary=----FormBoundary123', $body);
|
||||
|
||||
$this->assertSame([
|
||||
'valid' => 'validvalue',
|
||||
'another' => 'anothervalue',
|
||||
], $result);
|
||||
}
|
||||
|
||||
public function testParseMultipartWithSpacesInDisposition(): void
|
||||
{
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data ; name = \"test\" \r\n" .
|
||||
"\r\n" .
|
||||
"value\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $this->parser->parse('multipart/form-data; boundary=----FormBoundary123', $body);
|
||||
|
||||
$this->assertSame(['test' => 'value'], $result);
|
||||
}
|
||||
|
||||
// Security Tests
|
||||
|
||||
public function testFormDataSizeExceeded(): void
|
||||
{
|
||||
$config = new ParserConfig(maxFormDataSize: new Byte(50));
|
||||
$parser = $this->createFormDataParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('Form data size exceeded');
|
||||
|
||||
$longBody = str_repeat('a=value&', 20); // Creates a long form body
|
||||
$parser->parse('application/x-www-form-urlencoded', $longBody);
|
||||
}
|
||||
|
||||
public function testMultipartBoundaryTooLong(): void
|
||||
{
|
||||
$config = new ParserConfig(maxBoundaryLength: 10);
|
||||
$parser = $this->createFormDataParser($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\"\r\n\r\nvalue\r\n--{$longBoundary}--\r\n";
|
||||
$parser->parse("multipart/form-data; boundary={$longBoundary}", $body);
|
||||
}
|
||||
|
||||
public function testMultipartPartsCountExceeded(): void
|
||||
{
|
||||
$config = new ParserConfig(maxMultipartParts: 2);
|
||||
$parser = $this->createFormDataParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('Multipart parts exceeded');
|
||||
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"field1\"\r\n\r\nvalue1\r\n" .
|
||||
"------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"field2\"\r\n\r\nvalue2\r\n" .
|
||||
"------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"field3\"\r\n\r\nvalue3\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$parser->parse('multipart/form-data; boundary=----FormBoundary123', $body);
|
||||
}
|
||||
|
||||
public function testFieldCountExceeded(): void
|
||||
{
|
||||
$config = new ParserConfig(maxFieldCount: 2);
|
||||
$parser = $this->createFormDataParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('Field count exceeded');
|
||||
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"field1\"\r\n\r\nvalue1\r\n" .
|
||||
"------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"field2\"\r\n\r\nvalue2\r\n" .
|
||||
"------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"field3\"\r\n\r\nvalue3\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$parser->parse('multipart/form-data; boundary=----FormBoundary123', $body);
|
||||
}
|
||||
|
||||
public function testFieldNameTooLong(): void
|
||||
{
|
||||
$config = new ParserConfig(maxFieldNameLength: 10);
|
||||
$parser = $this->createFormDataParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('Field name too long');
|
||||
|
||||
$boundary = '----FormBoundary123';
|
||||
$longFieldName = str_repeat('a', 15);
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"{$longFieldName}\"\r\n\r\nvalue\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$parser->parse('multipart/form-data; boundary=----FormBoundary123', $body);
|
||||
}
|
||||
|
||||
public function testFieldValueTooLong(): void
|
||||
{
|
||||
$config = new ParserConfig(maxFieldValueLength: 10);
|
||||
$parser = $this->createFormDataParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('Field value too long');
|
||||
|
||||
$boundary = '----FormBoundary123';
|
||||
$longValue = str_repeat('a', 15);
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"test\"\r\n\r\n{$longValue}\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$parser->parse('multipart/form-data; boundary=----FormBoundary123', $body);
|
||||
}
|
||||
|
||||
public function testMaliciousScriptInjection(): void
|
||||
{
|
||||
$config = new ParserConfig(scanForMaliciousContent: true);
|
||||
$parser = $this->createFormDataParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('Suspicious content detected');
|
||||
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"evil\"\r\n\r\n<script>alert('xss')</script>\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$parser->parse('multipart/form-data; boundary=----FormBoundary123', $body);
|
||||
}
|
||||
|
||||
public function testSqlInjectionDetected(): void
|
||||
{
|
||||
$config = new ParserConfig(scanForMaliciousContent: true);
|
||||
$parser = $this->createFormDataParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('SQL injection attempt detected');
|
||||
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"query\"\r\n\r\nUNION SELECT * FROM users\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$parser->parse('multipart/form-data; boundary=----FormBoundary123', $body);
|
||||
}
|
||||
|
||||
public function testPathTraversalDetected(): void
|
||||
{
|
||||
$config = new ParserConfig(scanForMaliciousContent: true);
|
||||
$parser = $this->createFormDataParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('Path traversal attempt detected');
|
||||
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"path\"\r\n\r\n../../../etc/passwd\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$parser->parse('multipart/form-data; boundary=----FormBoundary123', $body);
|
||||
}
|
||||
|
||||
public function testControlCharactersDetected(): void
|
||||
{
|
||||
$config = new ParserConfig(scanForMaliciousContent: true);
|
||||
$parser = $this->createFormDataParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('Control characters detected');
|
||||
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"data\"\r\n\r\nvalue\x00nullbyte\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$parser->parse('multipart/form-data; boundary=----FormBoundary123', $body);
|
||||
}
|
||||
|
||||
public function testExcessiveRepetitionDetected(): void
|
||||
{
|
||||
$config = new ParserConfig(scanForMaliciousContent: true);
|
||||
$parser = $this->createFormDataParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('Excessive character repetition detected');
|
||||
|
||||
$boundary = '----FormBoundary123';
|
||||
$repetitiveValue = str_repeat('A', 150); // More than 100 repetitions
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"dos\"\r\n\r\n{$repetitiveValue}\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$parser->parse('multipart/form-data; boundary=----FormBoundary123', $body);
|
||||
}
|
||||
|
||||
// URL-encoded Security Tests
|
||||
|
||||
public function testUrlEncodedFormDataSizeExceeded(): void
|
||||
{
|
||||
$config = new ParserConfig(maxFormDataSize: new Byte(20));
|
||||
$parser = $this->createFormDataParser($config);
|
||||
|
||||
$this->expectException(ParserSecurityException::class);
|
||||
$this->expectExceptionMessage('Form data size exceeded');
|
||||
|
||||
$longBody = 'field=' . str_repeat('a', 50);
|
||||
$parser->parse('application/x-www-form-urlencoded', $longBody);
|
||||
}
|
||||
|
||||
// Security Configuration Tests
|
||||
|
||||
public function testSecurityDisabled(): void
|
||||
{
|
||||
$config = new ParserConfig(
|
||||
scanForMaliciousContent: false,
|
||||
maxFieldCount: 1000,
|
||||
maxFieldNameLength: 1000,
|
||||
maxFieldValueLength: 1000,
|
||||
maxFormDataSize: new Byte(10 * 1024 * 1024)
|
||||
);
|
||||
$parser = $this->createFormDataParser($config);
|
||||
|
||||
// Should not throw exception when security is disabled
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"evil\"\r\n\r\n<script>alert('xss')</script>\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $parser->parse('multipart/form-data; boundary=----FormBoundary123', $body);
|
||||
$this->assertSame(['evil' => '<script>alert(\'xss\')</script>'], $result);
|
||||
}
|
||||
|
||||
public function testWithinSecurityLimits(): void
|
||||
{
|
||||
$config = new ParserConfig(
|
||||
maxFieldCount: 5,
|
||||
maxFieldNameLength: 20,
|
||||
maxFieldValueLength: 50,
|
||||
maxFormDataSize: new Byte(1024),
|
||||
scanForMaliciousContent: true
|
||||
);
|
||||
$parser = $this->createFormDataParser($config);
|
||||
|
||||
// Should work fine within limits
|
||||
$boundary = '----FormBoundary123';
|
||||
$body = "------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"username\"\r\n\r\njohndoe\r\n" .
|
||||
"------FormBoundary123\r\n" .
|
||||
"Content-Disposition: form-data; name=\"email\"\r\n\r\njohn@example.com\r\n" .
|
||||
"------FormBoundary123--\r\n";
|
||||
|
||||
$result = $parser->parse('multipart/form-data; boundary=----FormBoundary123', $body);
|
||||
$this->assertSame([
|
||||
'username' => 'johndoe',
|
||||
'email' => 'john@example.com',
|
||||
], $result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user