$this->config->maxTotalHeaderSize->toBytes()) { throw ParserSecurityException::headerSizeExceeded( $headerSize, $this->config->maxTotalHeaderSize->toBytes() ); } $headers = []; $lines = explode("\r\n", $rawHeaders); // Security validation: Check header count $headerLineCount = count(array_filter($lines, fn ($line) => str_contains($line, ':'))); if ($headerLineCount > $this->config->maxHeaderCount) { throw ParserSecurityException::headerCountExceeded( $headerLineCount, $this->config->maxHeaderCount ); } foreach ($lines as $line) { if ($line === '') { // Empty line marks end of headers break; } // Skip the request/status line if (! str_contains($line, ':')) { continue; } [$name, $value] = explode(':', $line, 2); $name = trim($name); $value = trim($value); if ($name !== '') { // Security validation: Check header name length if (strlen($name) > $this->config->maxHeaderNameLength) { throw ParserSecurityException::headerNameTooLong( $name, strlen($name), $this->config->maxHeaderNameLength ); } // Security validation: Check header value length if (strlen($value) > $this->config->maxHeaderValueLength) { throw ParserSecurityException::headerValueTooLong( $name, strlen($value), $this->config->maxHeaderValueLength ); } // Security validation: Check for malicious content if ($this->config->scanForMaliciousContent) { $this->validateHeaderContent($name, $value); } if (isset($headers[$name])) { // Multiple headers with same name if (! is_array($headers[$name])) { $headers[$name] = [$headers[$name]]; } $headers[$name][] = $value; } else { $headers[$name] = $value; } } } return new Headers($headers); } /** * Parse headers from $_SERVER array * * @param array $server Server array * @return Headers * @throws ParserSecurityException */ public function parseFromServerArray(array $server): Headers { $headers = []; $totalHeaderSize = 0; foreach ($server as $key => $value) { // Convert scalar values to string if (! is_string($value)) { continue; } $headerName = null; // Standard headers with HTTP_ prefix if (str_starts_with($key, 'HTTP_')) { $headerName = $this->normalizeHeaderName(substr($key, 5)); $headerValue = $value; } // Special headers without HTTP_ prefix elseif (in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'])) { $headerName = $this->normalizeHeaderName($key); $headerValue = $value; } else { continue; } // Security validation: Check header name length if (strlen($headerName) > $this->config->maxHeaderNameLength) { throw ParserSecurityException::headerNameTooLong( $headerName, strlen($headerName), $this->config->maxHeaderNameLength ); } // Security validation: Check header value length if (strlen($headerValue) > $this->config->maxHeaderValueLength) { throw ParserSecurityException::headerValueTooLong( $headerName, strlen($headerValue), $this->config->maxHeaderValueLength ); } // Security validation: Check for malicious content if ($this->config->scanForMaliciousContent) { $this->validateHeaderContent($headerName, $headerValue); } $headers[$headerName] = $headerValue; $totalHeaderSize += strlen($headerName) + strlen($headerValue) + 4; // name: value\r\n } // Add Authorization header if present in different formats if (isset($server['PHP_AUTH_USER']) && isset($server['PHP_AUTH_PW'])) { $authHeader = 'Basic ' . base64_encode($server['PHP_AUTH_USER'] . ':' . $server['PHP_AUTH_PW']); // Security validation for generated auth header if (strlen($authHeader) > $this->config->maxHeaderValueLength) { throw ParserSecurityException::headerValueTooLong( 'Authorization', strlen($authHeader), $this->config->maxHeaderValueLength ); } $headers['Authorization'] = $authHeader; $totalHeaderSize += strlen('Authorization') + strlen($authHeader) + 4; } elseif (isset($server['PHP_AUTH_DIGEST'])) { $authHeader = 'Digest ' . $server['PHP_AUTH_DIGEST']; // Security validation for generated auth header if (strlen($authHeader) > $this->config->maxHeaderValueLength) { throw ParserSecurityException::headerValueTooLong( 'Authorization', strlen($authHeader), $this->config->maxHeaderValueLength ); } $headers['Authorization'] = $authHeader; $totalHeaderSize += strlen('Authorization') + strlen($authHeader) + 4; } // Security validation: Check header count if (count($headers) > $this->config->maxHeaderCount) { throw ParserSecurityException::headerCountExceeded( count($headers), $this->config->maxHeaderCount ); } // Security validation: Check total header size if ($totalHeaderSize > $this->config->maxTotalHeaderSize->toBytes()) { throw ParserSecurityException::headerSizeExceeded( $totalHeaderSize, $this->config->maxTotalHeaderSize->toBytes() ); } return new Headers($headers); } /** * Parse headers from getallheaders() output * * @param array $headers Headers from getallheaders() * @return Headers */ public function parseFromGetAllHeaders(array $headers): Headers { // getallheaders() returns headers in their original case // No transformation needed, just pass to Headers constructor return new Headers($headers); } /** * Get all headers from current request * Uses best available method * * @return Headers */ public function parseCurrentRequestHeaders(): Headers { // Prefer getallheaders() if available (Apache, FastCGI) if (function_exists('getallheaders')) { $headers = getallheaders(); if ($headers !== false) { return $this->parseFromGetAllHeaders($headers); } } // Fallback to $_SERVER parsing return $this->parseFromServerArray($_SERVER); } /** * Normalize header name from SERVER format * HTTP_CONTENT_TYPE -> Content-Type * * @param string $serverKey Server key without HTTP_ prefix * @return string Normalized header name */ private function normalizeHeaderName(string $serverKey): string { // Replace underscores with hyphens $headerName = str_replace('_', '-', $serverKey); // Convert to proper case (Content-Type, X-Forwarded-For, etc.) return implode('-', array_map('ucfirst', explode('-', strtolower($headerName)))); } /** * Parse Content-Type header to extract media type and parameters * * @param string $contentType Raw Content-Type header value * @return array{type: string, charset?: string, boundary?: string} */ public function parseContentType(string $contentType): array { $parts = explode(';', $contentType); $result = ['type' => trim(array_shift($parts))]; foreach ($parts as $part) { $part = trim($part); if (str_contains($part, '=')) { [$key, $value] = explode('=', $part, 2); $key = trim(strtolower($key)); $value = trim($value, ' "'); if ($key === 'charset' || $key === 'boundary') { $result[$key] = $value; } } } return $result; } /** * Validate header content for malicious patterns * * @throws ParserSecurityException */ private function validateHeaderContent(string $name, string $value): void { // Check for control characters (HTTP headers should not contain control chars) if (preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', $name . $value)) { throw ParserSecurityException::maliciousContentDetected( "Control characters detected in header '{$name}'" ); } // Check for CRLF injection (header splitting) if (preg_match('/\r|\n/', $name . $value)) { throw ParserSecurityException::maliciousContentDetected( "CRLF injection detected in header '{$name}'" ); } // Check for suspicious patterns in header values $maliciousPatterns = [ '/]*>/i', // Script injection '/javascript:/i', // JavaScript URL '/vbscript:/i', // VBScript URL '/on\w+\s*=/i', // Event handlers '/expression\s*\(/i', // CSS expression '/eval\s*\(/i', // JavaScript eval '/alert\s*\(/i', // JavaScript alert ]; foreach ($maliciousPatterns as $pattern) { if (preg_match($pattern, $value)) { throw ParserSecurityException::maliciousContentDetected( "Suspicious content detected in header '{$name}'" ); } } // Check for suspicious header names $suspiciousHeaderNames = [ 'X-XSS-Protection', // Might be used to disable XSS protection 'Content-Security-Policy', // Might be used to weaken CSP ]; foreach ($suspiciousHeaderNames as $suspiciousName) { if (stripos($name, $suspiciousName) !== false) { // Check for potentially dangerous values if (stripos($value, 'none') !== false || stripos($value, 'unsafe-inline') !== false) { throw ParserSecurityException::maliciousContentDetected( "Potentially dangerous security header value in '{$name}': '{$value}'" ); } } } // Check for excessively long base64 encoded values (potential data exfiltration) if (preg_match('/^[A-Za-z0-9+\/]+=*$/', $value) && strlen($value) > 1000) { throw ParserSecurityException::maliciousContentDetected( "Suspicious base64 encoded value in header '{$name}'" ); } } }