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