Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.79% covered (warning)
86.79%
46 / 53
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
IpBlockerMiddleware
86.79% covered (warning)
86.79%
46 / 53
66.67% covered (warning)
66.67%
2 / 3
8.15
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 process
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
1 / 1
5
 createBlockedResponse
61.11% covered (warning)
61.11%
11 / 18
0.00% covered (danger)
0.00%
0 / 1
2.24
1<?php
2declare(strict_types=1);
3
4namespace App\Middleware;
5
6use App\Service\IpSecurityService;
7use App\Utility\SettingsManager;
8use Cake\Http\Response;
9use Cake\Log\LogTrait;
10use Psr\Http\Message\ResponseInterface;
11use Psr\Http\Message\ServerRequestInterface;
12use Psr\Http\Server\MiddlewareInterface;
13use Psr\Http\Server\RequestHandlerInterface;
14
15/**
16 * IP Blocker Middleware
17 *
18 * Provides comprehensive IP-based security including:
19 * - Proper IP detection with proxy support
20 * - IP blocking based on database rules
21 * - Suspicious request pattern detection
22 * - Progressive blocking for repeat offenders
23 */
24class IpBlockerMiddleware implements MiddlewareInterface
25{
26    use LogTrait;
27
28    private IpSecurityService $ipSecurity;
29
30    /**
31     * Constructor
32     *
33     * @param \App\Service\IpSecurityService|null $ipSecurity IP security service instance
34     */
35    public function __construct(?IpSecurityService $ipSecurity = null)
36    {
37        $this->ipSecurity = $ipSecurity ?? new IpSecurityService();
38    }
39
40    /**
41     * Process the request through the middleware
42     *
43     * @param \Psr\Http\Message\ServerRequestInterface $request The request
44     * @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler
45     * @return \Psr\Http\Message\ResponseInterface A response
46     */
47    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
48    {
49        // Get client IP using the service
50        $clientIp = $this->ipSecurity->getClientIp($request);
51
52        // Check if we should block when IP cannot be determined
53        if (!$clientIp) {
54            $blockOnNoIp = SettingsManager::read('Security.blockOnNoIp', true);
55
56            if ($blockOnNoIp) {
57                $this->log('Request blocked: Unable to determine client IP', 'warning', ['group_name' => 'security']);
58
59                return $this->createBlockedResponse(
60                    __('Access Denied: Unable to verify request origin.'),
61                );
62            }
63
64            // If not blocking on no IP, allow the request but log it
65            $this->log(
66                'Request allowed despite missing IP - consider enabling blockOnNoIp',
67                'info',
68                ['group_name' => 'security'],
69            );
70
71            return $handler->handle($request);
72        }
73
74        // Store IP in request for use by other components (like RateLimitMiddleware)
75        $request = $request->withAttribute('clientIp', $clientIp);
76
77        // Check if IP is blocked
78        if ($this->ipSecurity->isIpBlocked($clientIp)) {
79            $this->log("Blocked request from banned IP: {$clientIp}", 'info', ['group_name' => 'security']);
80
81            return $this->createBlockedResponse(
82                __('Access Denied: Your IP address has been blocked due to suspicious activity.'),
83            );
84        }
85
86        // Check for suspicious patterns using the full request object
87        if ($this->ipSecurity->isSuspiciousRequest($request)) {
88            $uri = $request->getUri();
89            $route = $uri->getPath();
90            $query = $uri->getQuery() ?? '';
91
92            $this->ipSecurity->trackSuspiciousActivity($clientIp, $route, $query);
93
94            $this->log(
95                "Suspicious request detected from {$clientIp}{$route}?{$query}",
96                'warning',
97                ['group_name' => 'security'],
98            );
99
100            return $this->createBlockedResponse(
101                __('Access Denied: Suspicious request detected.'),
102            );
103        }
104
105        // Note: Rate limiting is now handled by RateLimitMiddleware
106
107        return $handler->handle($request);
108    }
109
110    /**
111     * Create a blocked response with appropriate format
112     *
113     * @param string $message The error message
114     * @param int $statusCode The HTTP status code
115     * @return \Psr\Http\Message\ResponseInterface The response
116     */
117    private function createBlockedResponse(string $message, int $statusCode = 403): ResponseInterface
118    {
119        $response = new Response();
120
121        // Set security headers
122        $response = $response
123            ->withHeader('X-Content-Type-Options', 'nosniff')
124            ->withHeader('X-Frame-Options', 'DENY')
125            ->withHeader('X-XSS-Protection', '1; mode=block');
126
127        // Check if request expects JSON
128        $acceptHeader = $_SERVER['HTTP_ACCEPT'] ?? '';
129        if (strpos($acceptHeader, 'application/json') !== false) {
130            return $response
131                ->withStatus($statusCode)
132                ->withType('application/json')
133                ->withStringBody(json_encode([
134                    'error' => $message,
135                    'code' => $statusCode,
136                ]));
137        }
138
139        // For HTML responses
140        return $response
141            ->withStatus($statusCode)
142            ->withType('text/html')
143            ->withStringBody($message);
144    }
145}