1
<?php
2
/**
3
 * This file is part of the Shieldon package.
4
 *
5
 * (c) Terry L. <contact@terryl.in>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 * 
10
 * php version 7.1.0
11
 * 
12
 * @category  Web-security
13
 * @package   Shieldon
14
 * @author    Terry Lin <contact@terryl.in>
15
 * @copyright 2019 terrylinooo
16
 * @license   https://github.com/terrylinooo/shieldon/blob/2.x/LICENSE MIT
17
 * @link      https://github.com/terrylinooo/shieldon
18
 * @see       https://shieldon.io
19
 */
20

21
declare(strict_types=1);
22

23
namespace Shieldon\Firewall\Kernel;
24

25
use Shieldon\Firewall\Kernel;
26
use function Shieldon\Firewall\get_request;
27
use function Shieldon\Firewall\get_session_instance;
28
use function Shieldon\Firewall\unset_superglobal;
29
use function time;
30
use function array_keys;
31

32
/*
33
 * This trait is used on Kernel only.
34
 */
35
trait FilterTrait
36
{
37
    /**
38
     *   Public methods       | Desctiotion
39
     *  ----------------------|---------------------------------------------
40
     *   setFilters           | Set the filters.
41
     *   setFilter            | Set a filter.
42
     *   disableFilters       | Disable all filters.
43
     *  ----------------------|---------------------------------------------
44
     */
45

46
    /**
47
     * Enable or disable the filters.
48
     *
49
     * @var array
50
     */
51
    protected $filterStatus = [
52
        /**
53
         * Check how many pageviews an user made in a short period time.
54
         * For example, limit an user can only view 30 pages in 60 minutes.
55
         */
56
        'frequency' => true,
57

58
        /**
59
         * If an user checks any internal link on your website, the user's
60
         * browser will generate HTTP_REFERER information.
61
         * When a user view many pages without HTTP_REFERER information meaning
62
         * that the user MUST be a web crawler.
63
         */
64
        'referer' => false,
65

66
        /**
67
         * Most of web crawlers do not render JavaScript, they only get the 
68
         * content they want, so we can check whether the cookie can be created
69
         * by JavaScript or not.
70
         */
71
        'cookie' => false,
72

73
        /**
74
         * Every unique user should only has a unique session, but if a user
75
         * creates different sessions every connection... meaning that the 
76
         * user's browser doesn't support cookie.
77
         * It is almost impossible that modern browsers not support cookie,
78
         * therefore the user MUST be a web crawler.
79
         */
80
        'session' => false,
81
    ];
82

83
    /**
84
     * The status for Filters to reset.
85
     *
86
     * @var array
87
     */
88
    protected $filterResetStatus = [
89
        's' => false, // second.
90
        'm' => false, // minute.
91
        'h' => false, // hour.
92
        'd' => false, // day.
93
    ];
94

95
    /**
96
     * Start an action for this IP address, allow or deny, and give a reason for it.
97
     *
98
     * @param int    $actionCode The action code. - 0: deny, 1: allow, 9: unban.
99
     * @param string $reasonCode The response code.
100
     * @param string $assignIp   The IP address.
101
     * 
102
     * @return void
103
     */
104
    abstract function action(int $actionCode, int $reasonCode, string $assignIp = ''): void;
105

106
    /**
107
     * Set the filters.
108
     *
109
     * @param array $settings filter settings.
110
     *
111
     * @return void
112
     */
113 3
    public function setFilters(array $settings)
114
    {
115 3
        foreach (array_keys($this->filterStatus) as $k) {
116 3
            if (isset($settings[$k])) {
117 3
                $this->filterStatus[$k] = $settings[$k] ?? false;
118
            }
119
        }
120
    }
121

122
    /**
123
     * Set a filter.
124
     *
125
     * @param string $filterName The filter's name.
126
     * @param bool   $value      True for enabling the filter, overwise.
127
     *
128
     * @return void
129
     */
130 3
    public function setFilter(string $filterName, bool $value): void
131
    {
132 3
        if (isset($this->filterStatus[$filterName])) {
133 3
            $this->filterStatus[$filterName] = $value;
134
        }
135
    }
136

137
    /**
138
     * Disable filters.
139
     * 
140
     * @return void
141
     */
142 3
    public function disableFilters(): void
143
    {
144 3
        $this->setFilters(
145
            [
146 3
                'session'   => false,
147
                'cookie'    => false,
148
                'referer'   => false,
149
                'frequency' => false,
150
            ]
151
        );
152
    }
153

154
    /*
155
    |--------------------------------------------------------------------------
156
    | Stage in Kernel
157
    |--------------------------------------------------------------------------
158
    | The below methods are used in "process" method in Kernel.
159
    */
160

161
    /**
162
     * Detect and analyze an user's behavior.
163
     *
164
     * @return int The response code.
165
     */
166 3
    protected function filter(): int
167
    {
168 3
        $now = time();
169 3
        $isFlagged = false;
170

171
        // Fetch an IP data from Shieldon log table.
172 3
        $ipDetail = $this->driver->get($this->ip, 'filter');
173

174 3
        $ipDetail = $this->driver->parseData($ipDetail, 'filter');
175 3
        $logData = $ipDetail;
176

177
        // Counting user pageviews.
178 3
        foreach (array_keys($this->filterResetStatus) as $unit) {
179

180
            // Each time unit will increase by 1.
181 3
            $logData['pageviews_' . $unit] = $ipDetail['pageviews_' . $unit] + 1;
182 3
            $logData['first_time_' . $unit] = $ipDetail['first_time_' . $unit];
183
        }
184

185 3
        $logData['first_time_flag'] = $ipDetail['first_time_flag'];
186

187 3
        if (!empty($ipDetail['ip'])) {
188 3
            $logData['ip'] = $this->ip;
189 3
            $logData['session'] = get_session_instance()->getId();
190 3
            $logData['hostname'] = $this->rdns;
191 3
            $logData['last_time'] = $now;
192

193
            // Start checking...
194 3
            foreach (array_keys($this->filterStatus) as $filter) {
195

196
                // For example: filterSession
197 3
                $method = 'filter' . ucfirst($filter);
198

199
                // For example: call $this->filterSession
200 3
                $filterReturnData = $this->{$method}($logData, $ipDetail, $isFlagged);
201

202
                // The log data will be updated by the filter.
203 3
                $logData = $filterReturnData['log_data'];
204

205
                // The flag will be passed to the next Filter.
206 3
                $isFlagged = $filterReturnData['is_flagged'];
207

208
                // If we find this session reached the filter limit, reject it.
209 3
                $isReject = $filterReturnData['is_reject'];
210

211 3
                if ($isReject) {
212 3
                    return kernel::RESPONSE_TEMPORARILY_DENY;
213
                }
214
            }
215

216
            // Is fagged as unusual beavior? Count the first time.
217 3
            if ($isFlagged) {
218 3
                $logData['first_time_flag'] = (!empty($logData['first_time_flag'])) ? $logData['first_time_flag'] : $now;
219
            }
220

221
            // Reset the flagged factor check.
222 3
            if (!empty($ipDetail['first_time_flag'])) {
223 3
                if ($now - $ipDetail['first_time_flag'] >= $this->properties['time_reset_limit']) {
224 3
                    $logData['flag_multi_session'] = 0;
225 3
                    $logData['flag_empty_referer'] = 0;
226 3
                    $logData['flag_js_cookie'] = 0;
227
                }
228
            }
229

230 3
            $this->driver->save($this->ip, $logData, 'filter');
231

232
        } else {
233

234
            // If $ipDetail[ip] is empty.
235
            // It means that the user is first time visiting our webiste.
236 3
            $this->InitializeFirstTimeFilter($logData);
237
        }
238

239 3
        return kernel::RESPONSE_ALLOW;
240
    }
241

242
    /*
243
    |--------------------------------------------------------------------------
244
    | The below methods are used only in "filter" method in current Trait.
245
    | See "Start checking..."
246
    |--------------------------------------------------------------------------
247
    */
248

249
    /**
250
     * When the user is first time visiting our webiste.
251
     * Initialize the log data.
252
     * 
253
     * @param array $logData The user's log data.
254
     *
255
     * @return void
256
     */
257 3
    protected function InitializeFirstTimeFilter($logData)
258
    {
259 3
        $now = time();
260

261 3
        $logData['ip']        = $this->ip;
262 3
        $logData['session']   = get_session_instance()->getId();
263 3
        $logData['hostname']  = $this->rdns;
264 3
        $logData['last_time'] = $now;
265

266 3
        foreach (array_keys($this->filterResetStatus) as $unit) {
267 3
            $logData['first_time_' . $unit] = $now;
268
        }
269

270 3
        $this->driver->save($this->ip, $logData, 'filter');
271
    }
272

273
    /**
274
     * Filter - Referer.
275
     *
276
     * @param array $logData   IP data from Shieldon log table.
277
     * @param array $ipDetail  The IP log data.
278
     * @param bool  $isFlagged Is flagged as unusual behavior or not.
279
     *
280
     * @return array
281
     */
282 3
    protected function filterReferer(array $logData, array $ipDetail, bool $isFlagged): array
283
    {
284 3
        $isReject = false;
285

286 3
        if ($this->filterStatus['referer']) {
287 3
            if ($logData['last_time'] - $ipDetail['last_time'] > $this->properties['interval_check_referer']) {
288

289
                // Get values from data table. We will count it and save it back to data table.
290
                // If an user is already in your website, it is impossible no referer when he views other pages.
291 3
                $logData['flag_empty_referer'] = $ipDetail['flag_empty_referer'];
292

293 3
                if (empty(get_request()->getHeaderLine('referer'))) {
294 3
                    $logData['flag_empty_referer']++;
295 3
                    $isFlagged = true;
296
                }
297

298
                // Ban this IP if they reached the limit.
299 3
                if ($logData['flag_empty_referer'] > $this->properties['limit_unusual_behavior']['referer']) {
300 3
                    $this->action(
301 3
                        kernel::ACTION_TEMPORARILY_DENY,
302 3
                        kernel::REASON_EMPTY_REFERER
303
                    );
304 3
                    $isReject = true;
305
                }
306
            }
307
        }
308

309
        return [
310 3
            'is_flagged' => $isFlagged,
311 3
            'is_reject' => $isReject,
312 3
            'log_data' => $logData,
313
        ];
314
    }
315

316
    /**
317
     * Filter - Session
318
     *
319
     * @param array $logData   IP data from Shieldon log table.
320
     * @param array $ipDetail  The IP log data.
321
     * @param bool  $isFlagged Is flagged as unusual behavior or not.
322
     *
323
     * @return array
324
     */
325 3
    protected function filterSession(array $logData, array $ipDetail, bool $isFlagged): array
326
    {
327 3
        $isReject = false;
328 3
        $sessionId = get_session_instance()->getId();
329

330 3
        if ($this->filterStatus['session']) {
331

332
            // Get values from data table. We will count it and save it back to data table.
333 3
            $logData['flag_multi_session'] = $ipDetail['flag_multi_session'];
334

335 3
            if ($sessionId !== $ipDetail['session']) {
336

337
                // Is is possible because of direct access by the same user many times.
338
                // Or they don't have session cookie set.
339 3
                $logData['flag_multi_session']++;
340 3
                $isFlagged = true;
341
            }
342

343
            // Ban this IP if they reached the limit.
344 3
            if ($logData['flag_multi_session'] > $this->properties['limit_unusual_behavior']['session']) {
345 3
                $this->action(
346 3
                    kernel::ACTION_TEMPORARILY_DENY,
347 3
                    kernel::REASON_TOO_MANY_SESSIONS
348
                );
349 3
                $isReject = true;
350
            }
351
        }
352

353
        return [
354 3
            'is_flagged' => $isFlagged,
355 3
            'is_reject' => $isReject,
356 3
            'log_data' => $logData,
357
        ];
358
    }
359

360
    /**
361
     * Filter - Cookie
362
     *
363
     * @param array $logData   IP data from Shieldon log table.
364
     * @param array $ipDetail  The IP log data.
365
     * @param bool  $isFlagged Is flagged as unusual behavior or not.
366
     *
367
     * @return array
368
     */
369 3
    protected function filterCookie(array $logData, array $ipDetail, bool $isFlagged): array
370
    {
371 3
        $isReject = false;
372

373
        // Let's checking cookie created by javascript..
374 3
        if ($this->filterStatus['cookie']) {
375

376
            // Get values from data table. We will count it and save it back to data table.
377 3
            $logData['flag_js_cookie'] = $ipDetail['flag_js_cookie'];
378 3
            $logData['pageviews_cookie'] = $ipDetail['pageviews_cookie'];
379

380 3
            $c = $this->properties['cookie_name'];
381

382 3
            $jsCookie = get_request()->getCookieParams()[$c] ?? 0;
383

384
            // Checking if a cookie is created by JavaScript.
385 3
            if (!empty($jsCookie)) {
386 3
                if ($jsCookie == '1') {
387 3
                    $logData['pageviews_cookie']++;
388

389
                } else {
390
                    // Flag it if the value is not 1.
391 3
                    $logData['flag_js_cookie']++;
392 3
                    $isFlagged = true;
393
                }
394
            } else {
395
                // If we cannot find the cookie, flag it.
396 3
                $logData['flag_js_cookie']++;
397 3
                $isFlagged = true;
398
            }
399

400 3
            if ($logData['flag_js_cookie'] > $this->properties['limit_unusual_behavior']['cookie']) {
401

402
                // Ban this IP if they reached the limit.
403 3
                $this->action(
404 3
                    kernel::ACTION_TEMPORARILY_DENY,
405 3
                    kernel::REASON_EMPTY_JS_COOKIE
406
                );
407 3
                $isReject = true;
408
            }
409

410
            // Remove JS cookie and reset.
411 3
            if ($logData['pageviews_cookie'] > $this->properties['limit_unusual_behavior']['cookie']) {
412 3
                $logData['pageviews_cookie'] = 0; // Reset to 0.
413 3
                $logData['flag_js_cookie'] = 0;
414 3
                unset_superglobal($c, 'cookie');
415
            }
416
        }
417

418
        return [
419 3
            'is_flagged' => $isFlagged,
420 3
            'is_reject' => $isReject,
421 3
            'log_data' => $logData,
422
        ];
423
    }
424

425
    /**
426
     * Filter - Frequency
427
     *
428
     * @param array $logData   IP data from Shieldon log table.
429
     * @param array $ipDetail  The IP log data.
430
     * @param bool  $isFlagged Is flagged as unusual behavior or not.
431
     *
432
     * @return array
433
     */
434 3
    protected function filterFrequency(array $logData, array $ipDetail, bool $isFlagged): array
435
    {
436 3
        $isReject = false;
437

438 3
        if ($this->filterStatus['frequency']) {
439 3
            $timeSecond = [];
440 3
            $timeSecond['s'] = 1;
441 3
            $timeSecond['m'] = 60;
442 3
            $timeSecond['h'] = 3600;
443 3
            $timeSecond['d'] = 86400;
444

445 3
            foreach (array_keys($this->properties['time_unit_quota']) as $unit) {
446

447 3
                if (($logData['last_time'] - $ipDetail['first_time_' . $unit]) >= ($timeSecond[$unit] + 1)) {
448

449
                    // For example:
450
                    // (1) minutely: now > first_time_m about 61, (2) hourly: now > first_time_h about 3601, 
451
                    // Let's prepare to rest the the pageview count.
452 3
                    $this->filterResetStatus[$unit] = true;
453

454
                } else {
455

456
                    // If an user's pageview count is more than the time period limit
457
                    // He or she will get banned.
458 3
                    if ($logData['pageviews_' . $unit] > $this->properties['time_unit_quota'][$unit]) {
459

460
                        $actionReason = [
461 3
                            's' => kernel::REASON_REACHED_LIMIT_SECOND,
462
                            'm' => kernel::REASON_REACHED_LIMIT_MINUTE,
463
                            'h' => kernel::REASON_REACHED_LIMIT_HOUR,
464
                            'd' => kernel::REASON_REACHED_LIMIT_DAY,
465
                        ];
466

467 3
                        $this->action(
468 3
                            kernel::ACTION_TEMPORARILY_DENY,
469 3
                            $actionReason[$unit]
470
                        );
471

472 3
                        $isReject = true;
473
                    }
474
                }
475
            }
476

477 3
            foreach ($this->filterResetStatus as $unit => $status) {
478
                // Reset the pageview check for specfic time unit.
479 3
                if ($status) {
480 3
                    $logData['first_time_' . $unit] = $logData['last_time'];
481 3
                    $logData['pageviews_' . $unit] = 0;
482
                }
483
            }
484
        }
485

486
        return [
487 3
            'is_flagged' => $isFlagged,
488 3
            'is_reject' => $isReject,
489 3
            'log_data' => $logData,
490
        ];
491
    }
492
}

Read our documentation on viewing source code .

Loading