Lines
57.40%
31 / 54
Methods
25.00%
2 / 8
Classes
0.00%
0 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| __construct | 85.71% | 6 / 7 | 0.00% | 0 / 1 | 3.03 | ||
| process | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 2 | ||
| sanitizeRequest | 63.63% | 7 / 11 | 0.00% | 0 / 1 | 3.43 | ||
| sanitizeData | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| sanitizeDataWithDepth | 70.00% | 7 / 10 | 0.00% | 0 / 1 | 5.68 | ||
| sanitizeObject | 0.00% | 0 / 1 | 0.00% | 0 / 1 | 2 | ||
| sanitizeObjectWithDepth | 0.00% | 0 / 10 | 0.00% | 0 / 1 | 42 | ||
| sanitizeString | 55.55% | 5 / 9 | 0.00% | 0 / 1 | 5.40 | ||
| 13 | class RequestSanitizerMiddleware implements MiddlewareInterface | |
| 14 | { | |
| 15 | private int $maxRecursionDepth; | |
| 16 | private int $maxStringLength; | |
| 17 | private LoggerService $logger; | |
| 18 | ||
| 19 | public function __construct(LoggerService $logger, int $maxRecursionDepth = 10, int $maxStringLength = 32768) | |
| 20 | { | |
| 21 | if ($maxRecursionDepth < 1) { | |
| 22 | throw new \InvalidArgumentException('maxRecursionDepth must be greater than 0'); | |
| 23 | } | |
| 24 | if ($maxStringLength < 1) { | |
| 25 | throw new \InvalidArgumentException('maxStringLength must be greater than 0'); | |
| 26 | } | |
| 27 | ||
| 28 | $this->logger = $logger; | |
| 29 | $this->maxRecursionDepth = $maxRecursionDepth; | |
| 30 | $this->maxStringLength = $maxStringLength; | |
| 31 | } | |
| 32 | ||
| 33 | #[\Override] | |
| 34 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface | |
| 35 | { | |
| 36 | try { | |
| 37 | $request = $this->sanitizeRequest($request); | |
| 38 | ||
| 39 | return $handler->handle($request); | |
| 40 | } catch (\Throwable $e) { | |
| 41 | $this->logger->logError($e, $request); | |
| 42 | throw $e; | |
| 43 | } | |
| 44 | } | |
| 45 | ||
| 46 | private function sanitizeRequest(ServerRequestInterface $request): ServerRequestInterface | |
| 47 | { | |
| 48 | $queryParams = $request->getQueryParams(); | |
| 49 | $sanitizedQueryParams = $this->sanitizeData($queryParams); | |
| 50 | $request = $request->withQueryParams($sanitizedQueryParams); | |
| 51 | ||
| 52 | $parsedBody = $request->getParsedBody(); | |
| 53 | if (is_array($parsedBody)) { | |
| 54 | $sanitizedParsedBody = $this->sanitizeData($parsedBody); | |
| 55 | $request = $request->withParsedBody($sanitizedParsedBody); | |
| 56 | } elseif (is_object($parsedBody)) { | |
| 57 | $sanitizedParsedBody = $this->sanitizeObject($parsedBody); | |
| 58 | $request = $request->withParsedBody($sanitizedParsedBody); | |
| 59 | } | |
| 60 | ||
| 61 | return $request; | |
| 62 | } | |
| 63 | ||
| 64 | private function sanitizeData(array $data): array | |
| 65 | { | |
| 66 | return $this->sanitizeDataWithDepth($data, 0); | |
| 67 | } | |
| 68 | ||
| 69 | private function sanitizeDataWithDepth(array $data, int $depth): array | |
| 70 | { | |
| 71 | if ($depth >= $this->maxRecursionDepth) { | |
| 72 | throw new \RuntimeException('Maximum recursion depth exceeded'); | |
| 73 | } | |
| 74 | ||
| 75 | $sanitized = []; | |
| 76 | foreach ($data as $key => $value) { | |
| 77 | if (is_array($value)) { | |
| 78 | $sanitized[$key] = $this->sanitizeDataWithDepth($value, $depth + 1); | |
| 79 | } elseif (is_string($value)) { | |
| 80 | $sanitized[$key] = $this->sanitizeString($value); | |
| 81 | } else { | |
| 82 | $sanitized[$key] = $value; | |
| 83 | } | |
| 84 | } | |
| 85 | return $sanitized; | |
| 86 | } | |
| 87 | ||
| 88 | private function sanitizeObject(object $data): object | |
| 89 | { | |
| 90 | return $this->sanitizeObjectWithDepth($data, 0); | |
| 91 | } | |
| 92 | ||
| 93 | private function sanitizeObjectWithDepth(object $data, int $depth): object | |
| 94 | { | |
| 95 | if ($depth >= $this->maxRecursionDepth) { | |
| 96 | throw new \RuntimeException('Maximum recursion depth exceeded'); | |
| 97 | } | |
| 98 | ||
| 99 | foreach ($data as $key => $value) { | |
| 100 | if (is_array($value)) { | |
| 101 | $data->$key = $this->sanitizeDataWithDepth($value, $depth + 1); | |
| 102 | } elseif (is_object($value)) { | |
| 103 | $data->$key = $this->sanitizeObjectWithDepth($value, $depth + 1); | |
| 104 | } elseif (is_string($value)) { | |
| 105 | $data->$key = $this->sanitizeString($value); | |
| 106 | } | |
| 107 | } | |
| 108 | return $data; | |
| 109 | } | |
| 110 | ||
| 111 | private function sanitizeString(string $value): string | |
| 112 | { | |
| 113 | if (strlen($value) > $this->maxStringLength) { | |
| 114 | throw new \RuntimeException('String exceeds maximum length'); | |
| 115 | } | |
| 116 | ||
| 117 | $value = preg_replace('/[\x00-\x1F\x7F]/u', '', $value) ?? ''; | |
| 118 | $value = trim($value); | |
| 119 | if (!mb_check_encoding($value, 'UTF-8')) { | |
| 120 | $this->logger->logWarning('Invalid string encoding detected.', ['value' => $value]); | |
| 121 | $encoded = mb_convert_encoding($value, 'UTF-8', 'auto'); | |
| 122 | $value = is_string($encoded) ? $encoded : $value; | |
| 123 | } | |
| 124 | return $value; | |
| 125 | } | |
| 126 | } |