Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
58.00% covered (warning)
58.00%
29 / 50
37.50% covered (danger)
37.50%
3 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
RequestSanitizerMiddleware
58.00% covered (warning)
58.00%
29 / 50
37.50% covered (danger)
37.50%
3 / 8
57.86
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 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\Zmscitizenapi\Middleware;
6
7use BO\Zmscitizenapi\Services\Core\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    public function __construct(LoggerService $logger)
19    {
20        $this->logger = $logger;
21        $config = \App::getRequestLimits();
22        $this->maxRecursionDepth = $config['maxRecursionDepth'];
23        $this->maxStringLength = $config['maxStringLength'];
24    }
25
26    #[\Override]
27    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
28    {
29        try {
30            $request = $this->sanitizeRequest($request);
31/*$this->logger->logInfo('Request sanitized', [
32                'uri' => (string) $request->getUri()
33            ]);*/
34
35            return $handler->handle($request);
36        } catch (\Throwable $e) {
37            $this->logger->logError($e, $request);
38            throw $e;
39        }
40    }
41
42    private function sanitizeRequest(ServerRequestInterface $request): ServerRequestInterface
43    {
44        // Sanitize query parameters
45        $queryParams = $request->getQueryParams();
46        $sanitizedQueryParams = $this->sanitizeData($queryParams);
47        $request = $request->withQueryParams($sanitizedQueryParams);
48// Sanitize parsed body
49        $parsedBody = $request->getParsedBody();
50        if (is_array($parsedBody)) {
51            $sanitizedParsedBody = $this->sanitizeData($parsedBody);
52            $request = $request->withParsedBody($sanitizedParsedBody);
53        } elseif (is_object($parsedBody)) {
54            $sanitizedParsedBody = $this->sanitizeObject($parsedBody);
55            $request = $request->withParsedBody($sanitizedParsedBody);
56        }
57
58        return $request;
59    }
60
61    private function sanitizeData(array $data): array
62    {
63        return $this->sanitizeDataWithDepth($data, 0);
64    }
65
66    private function sanitizeDataWithDepth(array $data, int $depth): array
67    {
68        if ($depth >= $this->maxRecursionDepth) {
69            throw new \RuntimeException('Maximum recursion depth exceeded');
70        }
71
72        $sanitized = [];
73        foreach ($data as $key => $value) {
74            if (is_array($value)) {
75                $sanitized[$key] = $this->sanitizeDataWithDepth($value, $depth + 1);
76            } elseif (is_string($value)) {
77                $sanitized[$key] = $this->sanitizeString($value);
78            } else {
79                $sanitized[$key] = $value;
80            }
81        }
82        return $sanitized;
83    }
84
85    private function sanitizeObject(object $data): object
86    {
87        return $this->sanitizeObjectWithDepth($data, 0);
88    }
89
90    private function sanitizeObjectWithDepth(object $data, int $depth): object
91    {
92        if ($depth >= $this->maxRecursionDepth) {
93            throw new \RuntimeException('Maximum recursion depth exceeded');
94        }
95
96        foreach ($data as $key => $value) {
97            if (is_array($value)) {
98                $data->$key = $this->sanitizeDataWithDepth($value, $depth + 1);
99            } elseif (is_object($value)) {
100                $data->$key = $this->sanitizeObjectWithDepth($value, $depth + 1);
101            } elseif (is_string($value)) {
102                $data->$key = $this->sanitizeString($value);
103            }
104        }
105        return $data;
106    }
107
108    private function sanitizeString(string $value): string
109    {
110        if (strlen($value) > $this->maxStringLength) {
111            throw new \RuntimeException('String exceeds maximum length');
112        }
113
114        $value = preg_replace('/[\x00-\x1F\x7F]/u', '', $value);
115        $value = trim($value);
116        if (!mb_check_encoding($value, 'UTF-8')) {
117            $this->logger->logWarning('Invalid string encoding detected.', ['value' => $value]);
118            $value = mb_convert_encoding($value, 'UTF-8', 'auto');
119        }
120        return $value;
121    }
122}