Middleware::uaCompatibleGuard()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 8
c 1
b 0
f 0
nc 4
nop 2
dl 0
loc 16
rs 10
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
Unused Code introduced by
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) {
0 ignored issues
show
introduced by
$response is always a sub-type of Symfony\Component\HttpFoundation\Response.
Loading history...
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
Unused Code introduced by
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) {
0 ignored issues
show
introduced by
$response is always a sub-type of Symfony\Component\HttpFoundation\Response.
Loading history...
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+
0 ignored issues
show
Bug introduced by
Are you sure the usage of header_remove('X-Powered-By') is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Security Best Practice introduced by
It seems like you do not handle an error condition for header_remove(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

129
            /** @scrutinizer ignore-unhandled */ @header_remove('X-Powered-By'); // PHP 5.3+

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
130
        } else {
131
            @ini_set('expose_php', 'off');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for ini_set(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

131
            /** @scrutinizer ignore-unhandled */ @ini_set('expose_php', 'off');

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
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
Unused Code introduced by
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) {
0 ignored issues
show
introduced by
$response is always a sub-type of Symfony\Component\HttpFoundation\Response.
Loading history...
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) {
0 ignored issues
show
introduced by
$response is always a sub-type of Symfony\Component\HttpFoundation\Response.
Loading history...
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
Unused Code introduced by
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) {
0 ignored issues
show
introduced by
$response is always a sub-type of Symfony\Component\HttpFoundation\Response.
Loading history...
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
Unused Code introduced by
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) {
0 ignored issues
show
introduced by
The condition $allow is always false.
Loading history...
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
Unused Code introduced by
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
Unused Code introduced by
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
Unused Code introduced by
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) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $clientIp of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
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