Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
58.49% covered (warning)
58.49%
31 / 53
25.00% covered (danger)
25.00%
2 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
RequestSanitizerMiddleware
58.49% covered (warning)
58.49%
31 / 53
25.00% covered (danger)
25.00%
2 / 8
65.20
0.00% covered (danger)
0.00%
0 / 1
 __construct
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 process
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 sanitizeRequest
63.64% covered (warning)
63.64%
7 / 11
0.00% covered (danger)
0.00%
0 / 1
3.43
 sanitizeData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sanitizeDataWithDepth
70.00% covered (warning)
70.00%
7 / 10
0.00% covered (danger)
0.00%
0 / 1
5.68
 sanitizeObject
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sanitizeObjectWithDepth
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 sanitizeString
62.50% covered (warning)
62.50%
5 / 8
0.00% covered (danger)
0.00%
0 / 1
3.47
1<?php
2
3declare(strict_types=1);
4
5namespace BO\Slim\Middleware;
6
7use BO\Slim\LoggerService;
8use Psr\Http\Message\ResponseInterface;
9use Psr\Http\Message\ServerRequestInterface;
10use Psr\Http\Server\MiddlewareInterface;
11use Psr\Http\Server\RequestHandlerInterface;
12
13class 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            $value = mb_convert_encoding($value, 'UTF-8', 'auto');
122        }
123        return $value;
124    }
125}