Issues (18)

src/Middleware.php (8 issues)

1
<?php
2
3
namespace HughCube\HttpSecurity;
4
5
use Closure;
6
use HughCube\HttpSecurity\Exceptions\ClientIpHasChangeHttpException;
7
use HughCube\HttpSecurity\Exceptions\IpAccessDeniedHttpException;
8
use HughCube\HttpSecurity\Exceptions\UserAgentHasChangeHttpException;
9
use Illuminate\Contracts\Config\Repository;
10
use Illuminate\Support\Str;
11
use ReflectionClass;
12
use Symfony\Component\HttpFoundation\IpUtils;
13
use Symfony\Component\HttpFoundation\Request;
14
use Symfony\Component\HttpFoundation\Response;
15
16
class Middleware
17
{
18
    /**
19
     * The config repository instance.
20
     *
21
     * @var \Illuminate\Contracts\Config\Repository
22
     */
23
    protected $config;
24
25
    /**
26
     * Create a new trusted proxies middleware instance.
27
     *
28
     * @param \Illuminate\Contracts\Config\Repository $config
29
     */
30
    public function __construct(Repository $config)
31
    {
32
        $this->config = $config;
33
    }
34
35
    /**
36
     * Handle an incoming request.
37
     *
38
     * @param \Illuminate\Http\Request $request
39
     * @param \Closure $next
40
     *
41
     * @return mixed
42
     * @throws \ReflectionException
43
     *
44
     * @throws \Symfony\Component\HttpKernel\Exception\HttpException
45
     */
46
    public function handle(Request $request, Closure $next)
47
    {
48
        $response = $next($request);
49
50
        /**
51
         * Call all guards.
52
         */
53
        $reflection = new ReflectionClass($this);
54
        foreach ($reflection->getMethods() as $method) {
55
            if (!$method->isPublic() || !Str::endsWith($method->getName(), 'Guard')) {
56
                continue;
57
            }
58
59
            $method->invokeArgs($this, [$request, $response]);
60
        }
61
62
        return $response;
63
    }
64
65
    /**
66
     * @param string $key
67
     *
68
     * @return mixed
69
     */
70
    protected function getGuardConfig($key, $default = null)
71
    {
72
        return $this->config->get("httpSecurity.{$key}", $default);
73
    }
74
75
    /**
76
     * Determine if a session driver has been configured.
77
     *
78
     * @param Request $request
79
     * @return bool
80
     */
81
    protected function sessionIsStarted(Request $request)
82
    {
83
        /** @var \Illuminate\Contracts\Session\Session $session */
84
        $session = $request->getSession();
85
86
        return $session->isStarted();
87
    }
88
89
    protected function buildCacheKey($key)
90
    {
91
        return "HttpSecurity:" . md5(serialize($key));
92
    }
93
94
    /**
95
     * @param Request $request
96
     * @param Response $response
97
     */
98
    public function contentMimeGuard($request, $response)
0 ignored issues
show
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

98
    public function contentMimeGuard(/** @scrutinizer ignore-unused */ $request, $response)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
99
    {
100
        if (false == $this->getGuardConfig('contentMime.enable')) {
101
            return;
102
        }
103
104
        if (!$response instanceof Response) {
105
            return;
106
        }
107
108
        $response->headers->set('X-Content-Type-Options', 'nosniff', false);
109
    }
110
111
    /**
112
     * @param Request $request
113
     * @param Response $response
114
     */
115
    public function poweredByHeaderGuard($request, $response)
0 ignored issues
show
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

115
    public function poweredByHeaderGuard(/** @scrutinizer ignore-unused */ $request, $response)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
116
    {
117
        if (false == $this->getGuardConfig('poweredByHeader.enable')) {
118
            return;
119
        }
120
121
        if (!$response instanceof Response) {
122
            return;
123
        }
124
125
        /**
126
         * Remove X-Powered-By header.
127
         */
128
        if (function_exists('header_remove')) {
129
            @header_remove('X-Powered-By'); // PHP 5.3+
130
        } else {
131
            @ini_set('expose_php', 'off');
132
        }
133
134
        $options = $this->getGuardConfig('poweredByHeader.options');
135
        if (null === $options) {
136
            return;
137
        }
138
139
        $response->headers->set('X-Powered-By', $options, false);
140
    }
141
142
    /**
143
     * @param Request $request
144
     * @param Response $response
145
     */
146
    public function uaCompatibleGuard($request, $response)
0 ignored issues
show
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

146
    public function uaCompatibleGuard(/** @scrutinizer ignore-unused */ $request, $response)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
147
    {
148
        if (false == $this->getGuardConfig('uaCompatible.enable')) {
149
            return;
150
        }
151
152
        if (!$response instanceof Response) {
153
            return;
154
        }
155
156
        $policy = $this->getGuardConfig('uaCompatible.policy');
157
        if (null === $policy) {
158
            return;
159
        }
160
161
        $response->headers->set('X-Ua-Compatible', $policy, false);
162
    }
163
164
    /**
165
     * @param Request $request
166
     * @param Response $response
167
     */
168
    public function hstsGuard($request, $response)
169
    {
170
        $enable = $this->getGuardConfig('hsts.enable');
171
        if (false == (null === $enable ? $request->isSecure() : $enable)) {
172
            return;
173
        }
174
175
        if (!$response instanceof Response) {
176
            return;
177
        }
178
179
        $maxAge = $this->getGuardConfig('hsts.maxAge', -1);
180
        if (0 >= $maxAge) {
181
            return;
182
        }
183
184
        $includeSubDomains = $this->getGuardConfig('hsts.includeSubDomains', false);
185
        $preload = $this->getGuardConfig('hsts.preload', false);
186
187
        $header = '';
188
        $header .= ("max-age={$maxAge}");
189
        $header .= ($includeSubDomains ? '; includeSubDomains' : '');
190
        $header .= ($preload ? '; preload' : '');
191
        $response->headers->set('Strict-Transport-Security', $header, false);
192
    }
193
194
    /**
195
     * @param Request $request
196
     * @param Response $response
197
     */
198
    public function xssProtectionGuard($request, $response)
0 ignored issues
show
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

198
    public function xssProtectionGuard(/** @scrutinizer ignore-unused */ $request, $response)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
199
    {
200
        if (false == $this->getGuardConfig('xssProtection.enable')) {
201
            return;
202
        }
203
204
        if (!$response instanceof Response) {
205
            return;
206
        }
207
208
        $policy = $this->getGuardConfig('xssProtection.policy');
209
        if (null === $policy) {
210
            return;
211
        }
212
213
        $response->headers->set('X-XSS-Protection', strval($policy), false);
214
    }
215
216
    /**
217
     * @param Request $request
218
     * @param Response $response
219
     */
220
    public function refererHotlinkingGuard($request, $response)
0 ignored issues
show
The parameter $response is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

220
    public function refererHotlinkingGuard($request, /** @scrutinizer ignore-unused */ $response)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
221
    {
222
        if (false == $this->getGuardConfig('refererHotlinking.enable')) {
223
            return;
224
        }
225
226
        $allow = false;
227
        $referer = $request->headers->get('Referer');
228
229
        // 如果 Referer 为空直接通过
230
        $allowEmpty = $this->getGuardConfig('refererHotlinking.allowEmpty', true);
231
        if (!$allow && $allowEmpty && null == $referer) {
232
            $allow = true;
233
        }
234
235
        // 去匹配允许的条件, 如果 allowedPatterns 为空直接通过
236
        $allowPatterns = $this->getGuardConfig('refererHotlinking.allowPatterns', []);
237
        $allow = $allow || empty($allowPatterns);
238
        foreach ($allowPatterns as $pathPattern => $refererPatterns) {
239
            if ($allow) {
240
                break;
241
            }
242
243
            if (!Str::is($pathPattern, $request->getPathInfo())) {
244
                continue;
245
            }
246
247
            $allow = Str::is($refererPatterns, $referer);
248
            break;
249
        }
250
251
        // 不允许的
252
        $forbidPatterns = $this->getGuardConfig('refererHotlinking.forbidPatterns', []);
253
        foreach ($forbidPatterns as $pathPattern => $refererPatterns) {
254
            if (!$allow) {
255
                break;
256
            }
257
258
            if (!Str::is($pathPattern, $request->getPathInfo())) {
259
                continue;
260
            }
261
262
            $allow = !Str::is($refererPatterns, $referer);
263
            break;
264
        }
265
266
        if (!$allow) {
267
            # throw new RefererHotlinkingHttpException("HTTP referer not allow");
268
        }
269
    }
270
271
    /**
272
     * @param Request $request
273
     * @param Response $response
274
     */
275
    public function clientIpChangeGuard($request, $response)
0 ignored issues
show
The parameter $response is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

275
    public function clientIpChangeGuard($request, /** @scrutinizer ignore-unused */ $response)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
276
    {
277
        if (false == $this->getGuardConfig('clientIpChange.enable')) {
278
            return;
279
        }
280
281
        if (!$this->sessionIsStarted($request)) {
282
            return;
283
        }
284
285
        $clientIpHash = crc32(serialize($request->getClientIp()));
286
287
        $sessionKey = $this->buildCacheKey(__METHOD__);
288
        if (!$request->getSession()->has($sessionKey)) {
289
            $request->getSession()->set($sessionKey, $clientIpHash);
290
        }
291
292
        if ($clientIpHash !== $request->getSession()->get($sessionKey)) {
293
            throw new ClientIpHasChangeHttpException('Ip has change.');
294
        }
295
    }
296
297
    /**
298
     * @param Request $request
299
     * @param Response $response
300
     */
301
    protected function userAgentChangeGuard($request, $response)
0 ignored issues
show
The parameter $response is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

301
    protected function userAgentChangeGuard($request, /** @scrutinizer ignore-unused */ $response)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
302
    {
303
        if (false == $this->getGuardConfig('userAgentChange.enable')) {
304
            return;
305
        }
306
307
        if (!$this->sessionIsStarted($request)) {
308
            return;
309
        }
310
311
        $userAgentHash = crc32(serialize($request->headers->get('User-Agent')));
312
313
        $sessionKey = $this->buildCacheKey(__METHOD__);
314
        if (!$request->getSession()->has($sessionKey)) {
315
            $request->getSession()->set($sessionKey, $userAgentHash);
316
        }
317
318
        if ($userAgentHash !== $request->getSession()->get($sessionKey)) {
319
            throw new UserAgentHasChangeHttpException('User-Agent has change.');
320
        }
321
    }
322
323
    /**
324
     * @param Request $request
325
     * @param Response $response
326
     */
327
    protected function ipAccessGuard($request, $response)
0 ignored issues
show
The parameter $response is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

327
    protected function ipAccessGuard($request, /** @scrutinizer ignore-unused */ $response)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
328
    {
329
        if (false == $this->getGuardConfig('ipAccess.enable')) {
330
            return;
331
        }
332
333
        $clientIp = $request->getClientIp();
334
        if (null == $clientIp) {
335
            return;
336
        }
337
338
        $allow = false;
339
340
        // 去匹配允许条件, 如果 allowedIps 为空直接通过
341
        $allowedIps = $this->getGuardConfig('ipAccess.allowedIps', []);
342
        $allow = ($allow || empty($allowedIps));
343
        foreach ($allowedIps as $pathPattern => $ipPatterns) {
344
            if ($allow) {
345
                break;
346
            }
347
348
            if (!Str::is($pathPattern, $request->getPathInfo())) {
349
                continue;
350
            }
351
352
            $allow = IpUtils::checkIp($clientIp, $ipPatterns);
353
            break;
354
        }
355
356
        // 不允许的
357
        $forbidPatterns = $this->getGuardConfig('ipAccess.forbidIps', []);
358
        foreach ($forbidPatterns as $pathPattern => $ipPatterns) {
359
            if (!$allow) {
360
                break;
361
            }
362
363
            if (!Str::is($pathPattern, $request->getPathInfo())) {
364
                continue;
365
            }
366
367
            $allow = !IpUtils::checkIp($clientIp, $ipPatterns);
368
            break;
369
        }
370
371
        if (!$allow) {
372
            throw new IpAccessDeniedHttpException('Not allowed ip.');
373
        }
374
    }
375
}
376