- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
460 lines
15 KiB
PHP
460 lines
15 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Framework\Http\Parser;
|
|
|
|
use App\Framework\Http\Parser\Exception\ParserSecurityException;
|
|
use App\Framework\Http\Parser\HeaderParser;
|
|
use App\Framework\Http\Parser\ParserConfig;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class HeaderParserTest extends TestCase
|
|
{
|
|
private HeaderParser $parser;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->parser = new HeaderParser();
|
|
}
|
|
|
|
public function testParseRawHeadersEmpty(): void
|
|
{
|
|
$result = $this->parser->parseRawHeaders('');
|
|
$this->assertSame([], $result->toArray());
|
|
}
|
|
|
|
public function testParseRawHeadersSimple(): void
|
|
{
|
|
$rawHeaders = "Content-Type: application/json\r\nContent-Length: 123\r\n";
|
|
$result = $this->parser->parseRawHeaders($rawHeaders);
|
|
|
|
$this->assertSame('application/json', $result->getFirst('Content-Type'));
|
|
$this->assertSame('123', $result->getFirst('Content-Length'));
|
|
}
|
|
|
|
public function testParseRawHeadersMultipleValues(): void
|
|
{
|
|
$rawHeaders = "Set-Cookie: session=abc\r\nSet-Cookie: user=xyz\r\n";
|
|
$result = $this->parser->parseRawHeaders($rawHeaders);
|
|
|
|
$cookies = $result->get('Set-Cookie');
|
|
$this->assertIsArray($cookies);
|
|
$this->assertSame(['session=abc', 'user=xyz'], $cookies);
|
|
}
|
|
|
|
public function testParseRawHeadersSkipsRequestLine(): void
|
|
{
|
|
$rawHeaders = "GET /test HTTP/1.1\r\nHost: example.com\r\n";
|
|
$result = $this->parser->parseRawHeaders($rawHeaders);
|
|
|
|
$this->assertSame('example.com', $result->getFirst('Host'));
|
|
$this->assertNull($result->getFirst('GET'));
|
|
}
|
|
|
|
public function testParseRawHeadersStopsAtEmptyLine(): void
|
|
{
|
|
$rawHeaders = "Host: example.com\r\n\r\nBody content here";
|
|
$result = $this->parser->parseRawHeaders($rawHeaders);
|
|
|
|
$this->assertSame('example.com', $result->getFirst('Host'));
|
|
$this->assertNull($result->getFirst('Body'));
|
|
}
|
|
|
|
public function testParseFromServerArrayStandard(): void
|
|
{
|
|
$server = [
|
|
'HTTP_HOST' => 'example.com',
|
|
'HTTP_USER_AGENT' => 'TestAgent/1.0',
|
|
'HTTP_ACCEPT' => 'application/json',
|
|
'HTTP_X_FORWARDED_FOR' => '192.168.1.1',
|
|
'REQUEST_METHOD' => 'GET', // Should be ignored
|
|
'SERVER_NAME' => 'example.com', // Should be ignored
|
|
];
|
|
|
|
$result = $this->parser->parseFromServerArray($server);
|
|
|
|
$this->assertSame('example.com', $result->getFirst('Host'));
|
|
$this->assertSame('TestAgent/1.0', $result->getFirst('User-Agent'));
|
|
$this->assertSame('application/json', $result->getFirst('Accept'));
|
|
$this->assertSame('192.168.1.1', $result->getFirst('X-Forwarded-For'));
|
|
$this->assertNull($result->getFirst('Request-Method'));
|
|
}
|
|
|
|
public function testParseFromServerArraySpecialHeaders(): void
|
|
{
|
|
$server = [
|
|
'CONTENT_TYPE' => 'application/json',
|
|
'CONTENT_LENGTH' => '1234',
|
|
'CONTENT_MD5' => 'abc123',
|
|
];
|
|
|
|
$result = $this->parser->parseFromServerArray($server);
|
|
|
|
$this->assertSame('application/json', $result->getFirst('Content-Type'));
|
|
$this->assertSame('1234', $result->getFirst('Content-Length'));
|
|
$this->assertSame('abc123', $result->getFirst('Content-Md5'));
|
|
}
|
|
|
|
public function testParseFromServerArrayBasicAuth(): void
|
|
{
|
|
$server = [
|
|
'PHP_AUTH_USER' => 'testuser',
|
|
'PHP_AUTH_PW' => 'testpass',
|
|
];
|
|
|
|
$result = $this->parser->parseFromServerArray($server);
|
|
|
|
$expected = 'Basic ' . base64_encode('testuser:testpass');
|
|
$this->assertSame($expected, $result->getFirst('Authorization'));
|
|
}
|
|
|
|
public function testParseFromServerArrayDigestAuth(): void
|
|
{
|
|
$server = [
|
|
'PHP_AUTH_DIGEST' => 'username="test", realm="api"',
|
|
];
|
|
|
|
$result = $this->parser->parseFromServerArray($server);
|
|
|
|
$this->assertSame('Digest username="test", realm="api"', $result->getFirst('Authorization'));
|
|
}
|
|
|
|
public function testParseFromServerArrayIgnoresNonStringValues(): void
|
|
{
|
|
$server = [
|
|
'HTTP_HOST' => 'example.com',
|
|
'HTTP_PORT' => 8080, // Integer should be ignored
|
|
'HTTP_ARRAY' => ['value1', 'value2'], // Array should be ignored
|
|
'HTTP_NULL' => null, // Null should be ignored
|
|
];
|
|
|
|
$result = $this->parser->parseFromServerArray($server);
|
|
|
|
$this->assertSame('example.com', $result->getFirst('Host'));
|
|
$this->assertNull($result->getFirst('Port'));
|
|
$this->assertNull($result->getFirst('Array'));
|
|
$this->assertNull($result->getFirst('Null'));
|
|
}
|
|
|
|
public function testParseContentTypeSimple(): void
|
|
{
|
|
$result = $this->parser->parseContentType('application/json');
|
|
|
|
$this->assertSame(['type' => 'application/json'], $result);
|
|
}
|
|
|
|
public function testParseContentTypeWithCharset(): void
|
|
{
|
|
$result = $this->parser->parseContentType('text/html; charset=utf-8');
|
|
|
|
$this->assertSame([
|
|
'type' => 'text/html',
|
|
'charset' => 'utf-8',
|
|
], $result);
|
|
}
|
|
|
|
public function testParseContentTypeWithBoundary(): void
|
|
{
|
|
$result = $this->parser->parseContentType('multipart/form-data; boundary=----FormBoundary123');
|
|
|
|
$this->assertSame([
|
|
'type' => 'multipart/form-data',
|
|
'boundary' => '----FormBoundary123',
|
|
], $result);
|
|
}
|
|
|
|
public function testParseContentTypeWithMultipleParameters(): void
|
|
{
|
|
$result = $this->parser->parseContentType('text/html; charset=utf-8; boundary=test; other=ignored');
|
|
|
|
$this->assertSame([
|
|
'type' => 'text/html',
|
|
'charset' => 'utf-8',
|
|
'boundary' => 'test',
|
|
], $result);
|
|
}
|
|
|
|
public function testParseContentTypeWithQuotedValues(): void
|
|
{
|
|
$result = $this->parser->parseContentType('multipart/form-data; boundary="----FormBoundary123"');
|
|
|
|
$this->assertSame([
|
|
'type' => 'multipart/form-data',
|
|
'boundary' => '----FormBoundary123',
|
|
], $result);
|
|
}
|
|
|
|
public function testParseContentTypeWithSpaces(): void
|
|
{
|
|
$result = $this->parser->parseContentType(' text/html ; charset = utf-8 ; boundary = test ');
|
|
|
|
$this->assertSame([
|
|
'type' => 'text/html',
|
|
'charset' => 'utf-8',
|
|
'boundary' => 'test',
|
|
], $result);
|
|
}
|
|
|
|
public function testNormalizeHeaderNameFromServer(): void
|
|
{
|
|
$server = [
|
|
'HTTP_CONTENT_TYPE' => 'application/json',
|
|
'HTTP_X_FORWARDED_FOR' => '192.168.1.1',
|
|
'HTTP_ACCEPT_ENCODING' => 'gzip',
|
|
'HTTP_USER_AGENT' => 'TestAgent',
|
|
];
|
|
|
|
$result = $this->parser->parseFromServerArray($server);
|
|
|
|
$this->assertSame('application/json', $result->getFirst('Content-Type'));
|
|
$this->assertSame('192.168.1.1', $result->getFirst('X-Forwarded-For'));
|
|
$this->assertSame('gzip', $result->getFirst('Accept-Encoding'));
|
|
$this->assertSame('TestAgent', $result->getFirst('User-Agent'));
|
|
}
|
|
|
|
// Security Tests
|
|
|
|
public function testHeaderCountLimitExceeded(): void
|
|
{
|
|
$config = new ParserConfig(maxHeaderCount: 2);
|
|
$parser = new HeaderParser($config);
|
|
|
|
$this->expectException(ParserSecurityException::class);
|
|
$this->expectExceptionMessage('Header count exceeded: 3 headers > 2 maximum');
|
|
|
|
$rawHeaders = "Header1: value1\r\nHeader2: value2\r\nHeader3: value3\r\n";
|
|
$parser->parseRawHeaders($rawHeaders);
|
|
}
|
|
|
|
public function testHeaderSizeExceeded(): void
|
|
{
|
|
$config = new ParserConfig(maxTotalHeaderSize: new \App\Framework\Core\ValueObjects\Byte(50));
|
|
$parser = new HeaderParser($config);
|
|
|
|
$this->expectException(ParserSecurityException::class);
|
|
$this->expectExceptionMessage('Total header size exceeded');
|
|
|
|
$longValue = str_repeat('x', 100);
|
|
$rawHeaders = "LongHeader: {$longValue}\r\n";
|
|
$parser->parseRawHeaders($rawHeaders);
|
|
}
|
|
|
|
public function testHeaderNameTooLong(): void
|
|
{
|
|
$config = new ParserConfig(maxHeaderNameLength: 10);
|
|
$parser = new HeaderParser($config);
|
|
|
|
$this->expectException(ParserSecurityException::class);
|
|
$this->expectExceptionMessage('Header name too long');
|
|
|
|
$rawHeaders = "VeryLongHeaderName: value\r\n";
|
|
$parser->parseRawHeaders($rawHeaders);
|
|
}
|
|
|
|
public function testHeaderValueTooLong(): void
|
|
{
|
|
$config = new ParserConfig(maxHeaderValueLength: 10);
|
|
$parser = new HeaderParser($config);
|
|
|
|
$this->expectException(ParserSecurityException::class);
|
|
$this->expectExceptionMessage('Header value too long');
|
|
|
|
$rawHeaders = "Header: verylongheadervaluethatexceedslimit\r\n";
|
|
$parser->parseRawHeaders($rawHeaders);
|
|
}
|
|
|
|
public function testMaliciousScriptInjection(): void
|
|
{
|
|
$config = new ParserConfig(scanForMaliciousContent: true);
|
|
$parser = new HeaderParser($config);
|
|
|
|
$this->expectException(ParserSecurityException::class);
|
|
$this->expectExceptionMessage('Suspicious content detected');
|
|
|
|
$rawHeaders = "XSS: <script>alert('xss')</script>\r\n";
|
|
$parser->parseRawHeaders($rawHeaders);
|
|
}
|
|
|
|
public function testMaliciousJavaScriptUrl(): void
|
|
{
|
|
$config = new ParserConfig(scanForMaliciousContent: true);
|
|
$parser = new HeaderParser($config);
|
|
|
|
$this->expectException(ParserSecurityException::class);
|
|
$this->expectExceptionMessage('Suspicious content detected');
|
|
|
|
$rawHeaders = "Redirect: javascript:alert('xss')\r\n";
|
|
$parser->parseRawHeaders($rawHeaders);
|
|
}
|
|
|
|
public function testControlCharactersDetected(): void
|
|
{
|
|
$config = new ParserConfig(scanForMaliciousContent: true);
|
|
$parser = new HeaderParser($config);
|
|
|
|
$this->expectException(ParserSecurityException::class);
|
|
$this->expectExceptionMessage('Control characters detected');
|
|
|
|
$rawHeaders = "Header: value\x00nullbyte\r\n";
|
|
$parser->parseRawHeaders($rawHeaders);
|
|
}
|
|
|
|
public function testCrlfInjectionDetected(): void
|
|
{
|
|
$config = new ParserConfig(scanForMaliciousContent: true);
|
|
$parser = new HeaderParser($config);
|
|
|
|
$this->expectException(ParserSecurityException::class);
|
|
$this->expectExceptionMessage('CRLF injection detected');
|
|
|
|
// CRLF injection within a single header value
|
|
$rawHeaders = "Header: value-with\r-crlf\r\n";
|
|
$parser->parseRawHeaders($rawHeaders);
|
|
}
|
|
|
|
public function testSuspiciousSecurityHeaderValue(): void
|
|
{
|
|
$config = new ParserConfig(scanForMaliciousContent: true);
|
|
$parser = new HeaderParser($config);
|
|
|
|
$this->expectException(ParserSecurityException::class);
|
|
$this->expectExceptionMessage('Potentially dangerous security header value');
|
|
|
|
$rawHeaders = "X-XSS-Protection: none\r\n";
|
|
$parser->parseRawHeaders($rawHeaders);
|
|
}
|
|
|
|
public function testSuspiciousBase64Value(): void
|
|
{
|
|
$config = new ParserConfig(scanForMaliciousContent: true);
|
|
$parser = new HeaderParser($config);
|
|
|
|
$this->expectException(ParserSecurityException::class);
|
|
$this->expectExceptionMessage('Suspicious base64 encoded value');
|
|
|
|
// Create a proper base64 string that's over 1000 characters
|
|
$longBase64 = str_repeat('A', 1001); // Simple base64-like string
|
|
$rawHeaders = "Data: {$longBase64}\r\n";
|
|
$parser->parseRawHeaders($rawHeaders);
|
|
}
|
|
|
|
// Server Array Security Tests
|
|
|
|
public function testServerArrayHeaderCountLimit(): void
|
|
{
|
|
$config = new ParserConfig(maxHeaderCount: 2);
|
|
$parser = new HeaderParser($config);
|
|
|
|
$this->expectException(ParserSecurityException::class);
|
|
$this->expectExceptionMessage('Header count exceeded');
|
|
|
|
$server = [
|
|
'HTTP_HEADER1' => 'value1',
|
|
'HTTP_HEADER2' => 'value2',
|
|
'HTTP_HEADER3' => 'value3',
|
|
];
|
|
|
|
$parser->parseFromServerArray($server);
|
|
}
|
|
|
|
public function testServerArrayHeaderNameTooLong(): void
|
|
{
|
|
$config = new ParserConfig(maxHeaderNameLength: 10);
|
|
$parser = new HeaderParser($config);
|
|
|
|
$this->expectException(ParserSecurityException::class);
|
|
$this->expectExceptionMessage('Header name too long');
|
|
|
|
$server = [
|
|
'HTTP_VERY_LONG_HEADER_NAME' => 'value',
|
|
];
|
|
|
|
$parser->parseFromServerArray($server);
|
|
}
|
|
|
|
public function testServerArrayHeaderValueTooLong(): void
|
|
{
|
|
$config = new ParserConfig(maxHeaderValueLength: 10);
|
|
$parser = new HeaderParser($config);
|
|
|
|
$this->expectException(ParserSecurityException::class);
|
|
$this->expectExceptionMessage('Header value too long');
|
|
|
|
$server = [
|
|
'HTTP_HEADER' => 'verylongheadervaluethatexceedslimit',
|
|
];
|
|
|
|
$parser->parseFromServerArray($server);
|
|
}
|
|
|
|
public function testServerArrayMaliciousContent(): void
|
|
{
|
|
$config = new ParserConfig(scanForMaliciousContent: true);
|
|
$parser = new HeaderParser($config);
|
|
|
|
$this->expectException(ParserSecurityException::class);
|
|
$this->expectExceptionMessage('Suspicious content detected');
|
|
|
|
$server = [
|
|
'HTTP_XSS' => '<script>alert("xss")</script>',
|
|
];
|
|
|
|
$parser->parseFromServerArray($server);
|
|
}
|
|
|
|
public function testAuthHeaderValueTooLong(): void
|
|
{
|
|
$config = new ParserConfig(maxHeaderValueLength: 20);
|
|
$parser = new HeaderParser($config);
|
|
|
|
$this->expectException(ParserSecurityException::class);
|
|
$this->expectExceptionMessage('Header value too long');
|
|
|
|
$server = [
|
|
'PHP_AUTH_USER' => 'verylongusernamethatexceedslimit',
|
|
'PHP_AUTH_PW' => 'verylongpasswordthatexceedslimit',
|
|
];
|
|
|
|
$parser->parseFromServerArray($server);
|
|
}
|
|
|
|
// Security Configuration Tests
|
|
|
|
public function testSecurityDisabled(): void
|
|
{
|
|
$config = new ParserConfig(
|
|
scanForMaliciousContent: false,
|
|
maxHeaderCount: 1000,
|
|
maxHeaderNameLength: 1000,
|
|
maxHeaderValueLength: 1000
|
|
);
|
|
$parser = new HeaderParser($config);
|
|
|
|
// Should not throw exception when security is disabled
|
|
$rawHeaders = "XSS: <script>alert('xss')</script>\r\n";
|
|
$result = $parser->parseRawHeaders($rawHeaders);
|
|
|
|
$this->assertSame('<script>alert(\'xss\')</script>', $result->getFirst('XSS'));
|
|
}
|
|
|
|
public function testWithinSecurityLimits(): void
|
|
{
|
|
$config = new ParserConfig(
|
|
maxHeaderCount: 5,
|
|
maxHeaderNameLength: 20,
|
|
maxHeaderValueLength: 50,
|
|
scanForMaliciousContent: true
|
|
);
|
|
$parser = new HeaderParser($config);
|
|
|
|
// Should work fine within limits
|
|
$rawHeaders = "Host: example.com\r\nUser-Agent: TestAgent/1.0\r\nAccept: application/json\r\n";
|
|
$result = $parser->parseRawHeaders($rawHeaders);
|
|
|
|
$this->assertSame('example.com', $result->getFirst('Host'));
|
|
$this->assertSame('TestAgent/1.0', $result->getFirst('User-Agent'));
|
|
$this->assertSame('application/json', $result->getFirst('Accept'));
|
|
}
|
|
}
|