├── .editorconfig
├── .gitattributes
├── .github
└── workflows
│ └── tests.yml
├── .gitignore
├── .styleci.yml
├── LICENSE.md
├── README.md
├── SECURITY.md
├── composer.json
├── phpunit.xml
├── src
├── Abstracts
│ └── Middleware.php
├── Commands
│ └── UnblockIp.php
├── Config
│ └── firewall.php
├── Events
│ └── AttackDetected.php
├── Exceptions
│ └── AccessDenied.php
├── Listeners
│ ├── BlockIp.php
│ ├── CheckLogin.php
│ └── NotifyUsers.php
├── Middleware
│ ├── Agent.php
│ ├── Bot.php
│ ├── Geo.php
│ ├── Ip.php
│ ├── Lfi.php
│ ├── Php.php
│ ├── Referrer.php
│ ├── Rfi.php
│ ├── Session.php
│ ├── Sqli.php
│ ├── Swear.php
│ ├── Url.php
│ ├── Whitelist.php
│ └── Xss.php
├── Migrations
│ ├── 2019_07_15_000000_create_firewall_ips_table.php
│ └── 2019_07_15_000000_create_firewall_logs_table.php
├── Models
│ ├── Ip.php
│ └── Log.php
├── Notifications
│ ├── AttackDetected.php
│ └── Notifiable.php
├── Provider.php
├── Resources
│ └── lang
│ │ └── en
│ │ ├── notifications.php
│ │ └── responses.php
└── Traits
│ └── Helper.php
└── tests
├── Feature
├── IpTest.php
├── LfiTest.php
├── RfiTest.php
├── SqliTest.php
├── WhitelistTest.php
└── XssTest.php
└── TestCase.php
/.editorconfig:
--------------------------------------------------------------------------------
1 | ; This file is for unifying the coding style for different editors and IDEs.
2 | ; More information at https://editorconfig.org
3 |
4 | root = true
5 |
6 | [*]
7 | charset = utf-8
8 | indent_size = 4
9 | indent_style = space
10 | end_of_line = lf
11 | insert_final_newline = true
12 | trim_trailing_whitespace = true
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
16 |
17 | [*.yml]
18 | indent_size = 2
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Set the default behavior, in case people don't have core.autocrlf set.
2 | * text eol=lf
3 |
4 | # Explicitly declare text files you want to always be normalized and converted
5 | # to native line endings on checkout.
6 | *.c text
7 | *.h text
8 |
9 | # Declare files that will always have CRLF line endings on checkout.
10 | *.sln text eol=crlf
11 |
12 | # Denote all files that are truly binary and should not be modified.
13 | *.png binary
14 | *.jpg binary
15 | *.otf binary
16 | *.eot binary
17 | *.svg binary
18 | *.ttf binary
19 | *.woff binary
20 | *.woff2 binary
21 |
22 | *.css linguist-vendored
23 | *.scss linguist-vendored
24 | *.js linguist-vendored
25 | CHANGELOG.md export-ignore
26 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | tests:
7 | name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.stability }}
8 |
9 | runs-on: ubuntu-latest
10 |
11 | strategy:
12 | matrix:
13 | php: ['8.0', '8.1', '8.2', '8.3', '8.4']
14 | laravel: [9.*, 10.*, 11.*, 12.*]
15 | stability: [prefer-lowest, prefer-stable]
16 | include:
17 | - laravel: 9.*
18 | testbench: 7.*
19 | - laravel: 10.*
20 | testbench: 8.*
21 | - laravel: 11.*
22 | testbench: 9.*
23 | - laravel: 12.*
24 | testbench: 10.*
25 | exclude:
26 | - laravel: 9.*
27 | php: 8.2
28 | - laravel: 9.*
29 | php: 8.3
30 | - laravel: 9.*
31 | php: 8.4
32 | - laravel: 10.*
33 | php: 8.0
34 | - laravel: 10.*
35 | php: 8.4
36 | - laravel: 11.*
37 | php: 8.0
38 | - laravel: 11.*
39 | php: 8.1
40 | - laravel: 12.*
41 | php: 8.0
42 | - laravel: 12.*
43 | php: 8.1
44 | steps:
45 | - name: Checkout code
46 | uses: actions/checkout@v3
47 |
48 | - name: Setup PHP
49 | uses: shivammathur/setup-php@v2
50 | with:
51 | php-version: ${{ matrix.php }}
52 | extensions: bcmath, ctype, dom, fileinfo, intl, gd, json, mbstring, pdo, pdo_sqlite, openssl, sqlite, xml, zip
53 | coverage: none
54 |
55 | - name: Install dependencies
56 | run: |
57 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
58 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction
59 |
60 | - name: Execute tests
61 | run: vendor/bin/phpunit
62 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | /.history
3 | /.vscode
4 | /tests/databases
5 | /vendor
6 | .DS_Store
7 | .phpunit.result.cache
8 | composer.phar
9 | composer.lock
10 |
--------------------------------------------------------------------------------
/.styleci.yml:
--------------------------------------------------------------------------------
1 | preset: psr2
2 |
3 | enabled:
4 | - concat_with_spaces
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Akaunting
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Web Application Firewall (WAF) package for Laravel
2 |
3 | 
4 | 
5 | [](https://styleci.io/repos/197242392)
6 | [](LICENSE.md)
7 |
8 | This package intends to protect your Laravel app from different type of attacks such as XSS, SQLi, RFI, LFI, User Agent, and a lot more. It will also block repeated attacks and send notification via email and/or slack when attack is detected. Furthermore, it will log failed logins and block the IP after a number of attempts.
9 |
10 | Note: Some middleware classes (i.e. Xss) are empty as the `Middleware` abstract class that they extend does all of the job, dynamically. In short, they all works ;)
11 |
12 | ## Getting Started
13 |
14 | ### 1. Install
15 |
16 | Run the following command:
17 |
18 | ```bash
19 | composer require akaunting/laravel-firewall
20 | ```
21 |
22 | ### 2. Publish
23 |
24 | Publish configuration, language, and migrations
25 |
26 | ```bash
27 | php artisan vendor:publish --tag=firewall
28 | ```
29 |
30 | ### 3. Database
31 |
32 | Create db tables
33 |
34 | ```bash
35 | php artisan migrate
36 | ```
37 |
38 | ### 4. Configure
39 |
40 | You can change the firewall settings of your app from `config/firewall.php` file
41 |
42 | ## Usage
43 |
44 | Middlewares are already defined so should just add them to routes. The `firewall.all` middleware applies all the middlewares available in the `all_middleware` array of config file.
45 |
46 | ```php
47 | Route::group(['middleware' => 'firewall.all'], function () {
48 | Route::get('/', 'HomeController@index');
49 | });
50 | ```
51 |
52 | You can apply each middleware per route. For example, you can allow only whitelisted IPs to access admin:
53 |
54 | ```php
55 | Route::group(['middleware' => 'firewall.whitelist'], function () {
56 | Route::get('/admin', 'AdminController@index');
57 | });
58 | ```
59 |
60 | Or you can get notified when anyone NOT in `whitelist` access admin, by adding it to the `inspections` config:
61 |
62 | ```php
63 | Route::group(['middleware' => 'firewall.url'], function () {
64 | Route::get('/admin', 'AdminController@index');
65 | });
66 | ```
67 |
68 | Available middlewares applicable to routes:
69 |
70 | ```php
71 | firewall.all
72 |
73 | firewall.agent
74 | firewall.bot
75 | firewall.geo
76 | firewall.ip
77 | firewall.lfi
78 | firewall.php
79 | firewall.referrer
80 | firewall.rfi
81 | firewall.session
82 | firewall.sqli
83 | firewall.swear
84 | firewall.url
85 | firewall.whitelist
86 | firewall.xss
87 | ```
88 |
89 | You may also define `routes` for each middleware in `config/firewall.php` and apply that middleware or `firewall.all` at the top of all routes.
90 |
91 | ## Notifications
92 |
93 | Firewall will send a notification as soon as an attack has been detected. Emails entered in `notifications.email.to` config must be valid Laravel users in order to send notifications. Check out the Notifications documentation of Laravel for further information.
94 |
95 | ## Changelog
96 |
97 | Please see [Releases](../../releases) for more information on what has changed recently.
98 |
99 | ## Contributing
100 |
101 | Pull requests are more than welcome. You must follow the PSR coding standards.
102 |
103 | ## Security
104 |
105 | Please review [our security policy](https://github.com/akaunting/laravel-firewall/security/policy) on how to report security vulnerabilities.
106 |
107 | ## Credits
108 |
109 | - [Denis Duliçi](https://github.com/denisdulici)
110 | - [All Contributors](../../contributors)
111 |
112 | ## License
113 |
114 | The MIT License (MIT). Please see [LICENSE](LICENSE.md) for more information.
115 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | **PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, [SEE BELOW](#reporting-a-vulnerability).**
4 |
5 | ## Reporting a Vulnerability
6 |
7 | If you discover any security related issues, please email security@akaunting.com instead of using the issue tracker.
8 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "akaunting/laravel-firewall",
3 | "description": "Web Application Firewall (WAF) package for Laravel",
4 | "keywords": [
5 | "laravel",
6 | "firewall",
7 | "security",
8 | "waf",
9 | "blacklist",
10 | "xss",
11 | "sqli",
12 | "rfi",
13 | "lfi"
14 | ],
15 | "license": "MIT",
16 | "authors": [
17 | {
18 | "name": "Denis Duliçi",
19 | "email": "info@akaunting.com",
20 | "homepage": "https://akaunting.com",
21 | "role": "Developer"
22 | }
23 | ],
24 | "require": {
25 | "php": "^8.0",
26 | "laravel/framework": "^9.0|^10.0|^11.0|^12.0",
27 | "guzzlehttp/guzzle": "^7.8",
28 | "jenssegers/agent": "2.6.*"
29 | },
30 | "require-dev": {
31 | "phpunit/phpunit": "^9.5|^10.0|^11.0",
32 | "orchestra/testbench": "^7.4|^8.0|^9.0|^10.0"
33 | },
34 | "autoload": {
35 | "psr-4": {
36 | "Akaunting\\Firewall\\": "src"
37 | }
38 | },
39 | "autoload-dev": {
40 | "psr-4": {
41 | "Akaunting\\Firewall\\Tests\\": "tests"
42 | }
43 | },
44 | "extra": {
45 | "laravel": {
46 | "providers": [
47 | "Akaunting\\Firewall\\Provider"
48 | ]
49 | }
50 | },
51 | "scripts": {
52 | "test": "vendor/bin/phpunit"
53 | },
54 | "config": {
55 | "allow-plugins": {
56 | "composer/package-versions-deprecated": true
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 | ./src
12 |
13 |
14 |
15 |
16 | tests
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/Abstracts/Middleware.php:
--------------------------------------------------------------------------------
1 | skip($request)) {
25 | return $next($request);
26 | }
27 |
28 | if ($this->check($this->getPatterns())) {
29 | return $this->respond(config('firewall.responses.block'));
30 | }
31 |
32 | return $next($request);
33 | }
34 |
35 | public function skip($request)
36 | {
37 | $this->prepare($request);
38 |
39 | if ($this->isDisabled()) {
40 | return true;
41 | }
42 |
43 | if ($this->isWhitelist()) {
44 | return true;
45 | }
46 |
47 | if (! $this->isMethod()) {
48 | return true;
49 | }
50 |
51 | if ($this->isRoute()) {
52 | return true;
53 | }
54 |
55 | return false;
56 | }
57 |
58 | public function prepare($request)
59 | {
60 | $this->request = $request;
61 | $this->middleware = strtolower((new \ReflectionClass($this))->getShortName());
62 | $this->user_id = auth()->id() ?: 0;
63 | }
64 |
65 | public function getPatterns()
66 | {
67 | return config('firewall.middleware.' . $this->middleware . '.patterns', []);
68 | }
69 |
70 | public function check($patterns)
71 | {
72 | $log = null;
73 |
74 | foreach ($patterns as $pattern) {
75 | if (! $match = $this->match($pattern, $this->request->input())) {
76 | continue;
77 | }
78 |
79 | $log = $this->log();
80 |
81 | event(new AttackDetected($log));
82 |
83 | break;
84 | }
85 |
86 | if ($log) {
87 | return true;
88 | }
89 |
90 | return false;
91 | }
92 |
93 | public function match($pattern, $input)
94 | {
95 | $result = false;
96 |
97 | if (! is_array($input) && !is_string($input)) {
98 | return false;
99 | }
100 |
101 | if (! is_array($input)) {
102 | $input = $this->prepareInput($input);
103 |
104 | return preg_match($pattern, $input);
105 | }
106 |
107 | foreach ($input as $key => $value) {
108 | if (empty($value)) {
109 | continue;
110 | }
111 |
112 | if (is_array($value)) {
113 | if (!$result = $this->match($pattern, $value)) {
114 | continue;
115 | }
116 |
117 | break;
118 | }
119 |
120 | if (! $this->isInput($key)) {
121 | continue;
122 | }
123 |
124 | $value = $this->prepareInput($value);
125 |
126 | if (! $result = preg_match($pattern, $value)) {
127 | continue;
128 | }
129 |
130 | break;
131 | }
132 |
133 | return $result;
134 | }
135 |
136 | public function prepareInput($value)
137 | {
138 | return $value;
139 | }
140 |
141 | public function respond($response, $data = [])
142 | {
143 | if ($response['code'] == 200) {
144 | return '';
145 | }
146 |
147 | if ($view = $response['view']) {
148 | return Response::view($view, $data, $response['code']);
149 | }
150 |
151 | if ($redirect = $response['redirect']) {
152 | if (($this->middleware == 'ip') && $this->request->is($redirect)) {
153 | abort($response['code'], trans('firewall::responses.block.message'));
154 | }
155 |
156 | return Redirect::to($redirect);
157 | }
158 |
159 | if ($response['abort']) {
160 | abort($response['code'], trans('firewall::responses.block.message'));
161 | }
162 |
163 | if (array_key_exists('exception', $response)) {
164 | if ($exception = $response['exception']) {
165 | throw new $exception();
166 | }
167 | }
168 |
169 | return Response::make(trans('firewall::responses.block.message'), $response['code']);
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/src/Commands/UnblockIp.php:
--------------------------------------------------------------------------------
1 | blocked()->each(function ($ip) use ($now) {
44 | if (empty($ip->log)) {
45 | return;
46 | }
47 |
48 | $period = config('firewall.middleware.' . $ip->log->middleware . '.auto_block.period');
49 |
50 | if ($ip->created_at->addSeconds($period) > $now) {
51 | return;
52 | }
53 |
54 | $ip->logs()->delete();
55 | $ip->delete();
56 | });
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Config/firewall.php:
--------------------------------------------------------------------------------
1 | env('FIREWALL_ENABLED', true),
6 |
7 | 'whitelist' => explode(',', env('FIREWALL_WHITELIST', '')),
8 |
9 | 'models' => [
10 | 'user' => '\App\Models\User',
11 | // 'log' => '\App\Models\YourLogModel',
12 | // 'ip' => '\App\Models\YourIpModel',
13 | ],
14 |
15 | 'log' => [
16 | 'max_request_size' => 2048,
17 | ],
18 |
19 | 'cron' => [
20 | 'enabled' => env('FIREWALL_CRON_ENABLED', true),
21 | 'expression' => env('FIREWALL_CRON_EXPRESSION', '* * * * *'),
22 | ],
23 |
24 | 'responses' => [
25 |
26 | 'block' => [
27 | 'view' => env('FIREWALL_BLOCK_VIEW', null),
28 | 'redirect' => env('FIREWALL_BLOCK_REDIRECT', null),
29 | 'abort' => env('FIREWALL_BLOCK_ABORT', false),
30 | 'code' => env('FIREWALL_BLOCK_CODE', 403),
31 | //'exception' => \Akaunting\Firewall\Exceptions\AccessDenied::class,
32 | ],
33 |
34 | ],
35 |
36 | 'notifications' => [
37 |
38 | 'mail' => [
39 | 'enabled' => env('FIREWALL_EMAIL_ENABLED', false),
40 | 'name' => env('FIREWALL_EMAIL_NAME', 'Laravel Firewall'),
41 | 'from' => env('FIREWALL_EMAIL_FROM', 'firewall@mydomain.com'),
42 | 'to' => env('FIREWALL_EMAIL_TO', 'admin@mydomain.com'),
43 | 'queue' => env('FIREWALL_EMAIL_QUEUE', 'default'),
44 | ],
45 |
46 | 'slack' => [
47 | 'enabled' => env('FIREWALL_SLACK_ENABLED', false),
48 | 'emoji' => env('FIREWALL_SLACK_EMOJI', ':fire:'),
49 | 'from' => env('FIREWALL_SLACK_FROM', 'Laravel Firewall'),
50 | 'to' => env('FIREWALL_SLACK_TO'), // webhook url
51 | 'channel' => env('FIREWALL_SLACK_CHANNEL', null), // set null to use the default channel of webhook
52 | 'queue' => env('FIREWALL_SLACK_QUEUE', 'default'),
53 | ],
54 |
55 | ],
56 |
57 | 'all_middleware' => [
58 | 'firewall.ip',
59 | 'firewall.agent',
60 | 'firewall.bot',
61 | 'firewall.geo',
62 | 'firewall.lfi',
63 | 'firewall.php',
64 | 'firewall.referrer',
65 | 'firewall.rfi',
66 | 'firewall.session',
67 | 'firewall.sqli',
68 | 'firewall.swear',
69 | 'firewall.xss',
70 | //'App\Http\Middleware\YourCustomRule',
71 | ],
72 |
73 | 'middleware' => [
74 |
75 | 'ip' => [
76 | 'enabled' => env('FIREWALL_MIDDLEWARE_IP_ENABLED', env('FIREWALL_ENABLED', true)),
77 |
78 | 'methods' => ['all'],
79 |
80 | 'routes' => [
81 | 'only' => [], // i.e. 'contact'
82 | 'except' => [], // i.e. 'admin/*'
83 | ],
84 | ],
85 |
86 | 'agent' => [
87 | 'enabled' => env('FIREWALL_MIDDLEWARE_AGENT_ENABLED', env('FIREWALL_ENABLED', true)),
88 |
89 | 'methods' => ['all'],
90 |
91 | 'routes' => [
92 | 'only' => [], // i.e. 'contact'
93 | 'except' => [], // i.e. 'admin/*'
94 | ],
95 |
96 | // https://github.com/jenssegers/agent
97 | 'browsers' => [
98 | 'allow' => [], // i.e. 'Chrome', 'Firefox'
99 | 'block' => [], // i.e. 'IE'
100 | ],
101 |
102 | 'platforms' => [
103 | 'allow' => [], // i.e. 'Ubuntu', 'Windows'
104 | 'block' => [], // i.e. 'OS X'
105 | ],
106 |
107 | 'devices' => [
108 | 'allow' => [], // i.e. 'Desktop', 'Mobile'
109 | 'block' => [], // i.e. 'Tablet'
110 | ],
111 |
112 | 'properties' => [
113 | 'allow' => [], // i.e. 'Gecko', 'Version/5.1.7'
114 | 'block' => [], // i.e. 'AppleWebKit'
115 | ],
116 |
117 | 'auto_block' => [
118 | 'attempts' => 5,
119 | 'frequency' => 1 * 60, // 1 minute
120 | 'period' => 30 * 60, // 30 minutes
121 | ],
122 | ],
123 |
124 | 'bot' => [
125 | 'enabled' => env('FIREWALL_MIDDLEWARE_BOT_ENABLED', env('FIREWALL_ENABLED', true)),
126 |
127 | 'methods' => ['all'],
128 |
129 | 'routes' => [
130 | 'only' => [], // i.e. 'contact'
131 | 'except' => [], // i.e. 'admin/*'
132 | ],
133 |
134 | // https://github.com/JayBizzle/Crawler-Detect/blob/master/raw/Crawlers.txt
135 | 'crawlers' => [
136 | 'allow' => [], // i.e. 'GoogleSites', 'GuzzleHttp'
137 | 'block' => [], // i.e. 'Holmes'
138 | ],
139 |
140 | 'auto_block' => [
141 | 'attempts' => 5,
142 | 'frequency' => 1 * 60, // 1 minute
143 | 'period' => 30 * 60, // 30 minutes
144 | ],
145 | ],
146 |
147 | 'geo' => [
148 | 'enabled' => env('FIREWALL_MIDDLEWARE_GEO_ENABLED', env('FIREWALL_ENABLED', true)),
149 |
150 | 'methods' => ['all'],
151 |
152 | 'routes' => [
153 | 'only' => [], // i.e. 'contact'
154 | 'except' => [], // i.e. 'admin/*'
155 | ],
156 |
157 | 'continents' => [
158 | 'allow' => [], // i.e. 'Africa'
159 | 'block' => [], // i.e. 'Europe'
160 | ],
161 |
162 | 'regions' => [
163 | 'allow' => [], // i.e. 'California'
164 | 'block' => [], // i.e. 'Nevada'
165 | ],
166 |
167 | 'countries' => [
168 | 'allow' => [], // i.e. 'Albania'
169 | 'block' => [], // i.e. 'Madagascar'
170 | ],
171 |
172 | 'cities' => [
173 | 'allow' => [], // i.e. 'Istanbul'
174 | 'block' => [], // i.e. 'London'
175 | ],
176 |
177 | // ipapi, extremeiplookup, ipstack, ipdata, ipinfo, ipregistry, ip2locationio
178 | 'service' => 'ipapi',
179 |
180 | 'auto_block' => [
181 | 'attempts' => 3,
182 | 'frequency' => 5 * 60, // 5 minutes
183 | 'period' => 30 * 60, // 30 minutes
184 | ],
185 | ],
186 |
187 | 'lfi' => [
188 | 'enabled' => env('FIREWALL_MIDDLEWARE_LFI_ENABLED', env('FIREWALL_ENABLED', true)),
189 |
190 | 'methods' => ['get', 'delete'],
191 |
192 | 'routes' => [
193 | 'only' => [], // i.e. 'contact'
194 | 'except' => [], // i.e. 'admin/*'
195 | ],
196 |
197 | 'inputs' => [
198 | 'only' => [], // i.e. 'first_name'
199 | 'except' => [], // i.e. 'password'
200 | ],
201 |
202 | 'patterns' => [
203 | '#\.\/#is',
204 | ],
205 |
206 | 'auto_block' => [
207 | 'attempts' => 3,
208 | 'frequency' => 5 * 60, // 5 minutes
209 | 'period' => 30 * 60, // 30 minutes
210 | ],
211 | ],
212 |
213 | 'login' => [
214 | 'enabled' => env('FIREWALL_MIDDLEWARE_LOGIN_ENABLED', env('FIREWALL_ENABLED', true)),
215 |
216 | 'auto_block' => [
217 | 'attempts' => 5,
218 | 'frequency' => 1 * 60, // 1 minute
219 | 'period' => 30 * 60, // 30 minutes
220 | ],
221 | ],
222 |
223 | 'php' => [
224 | 'enabled' => env('FIREWALL_MIDDLEWARE_PHP_ENABLED', env('FIREWALL_ENABLED', true)),
225 |
226 | 'methods' => ['get', 'post', 'delete'],
227 |
228 | 'routes' => [
229 | 'only' => [], // i.e. 'contact'
230 | 'except' => [], // i.e. 'admin/*'
231 | ],
232 |
233 | 'inputs' => [
234 | 'only' => [], // i.e. 'first_name'
235 | 'except' => [], // i.e. 'password'
236 | ],
237 |
238 | 'patterns' => [
239 | 'bzip2://',
240 | 'expect://',
241 | 'glob://',
242 | 'phar://',
243 | 'php://',
244 | 'ogg://',
245 | 'rar://',
246 | 'ssh2://',
247 | 'zip://',
248 | 'zlib://',
249 | ],
250 |
251 | 'auto_block' => [
252 | 'attempts' => 3,
253 | 'frequency' => 5 * 60, // 5 minutes
254 | 'period' => 30 * 60, // 30 minutes
255 | ],
256 | ],
257 |
258 | 'referrer' => [
259 | 'enabled' => env('FIREWALL_MIDDLEWARE_REFERRER_ENABLED', env('FIREWALL_ENABLED', true)),
260 |
261 | 'methods' => ['all'],
262 |
263 | 'routes' => [
264 | 'only' => [], // i.e. 'contact'
265 | 'except' => [], // i.e. 'admin/*'
266 | ],
267 |
268 | 'blocked' => [],
269 |
270 | 'auto_block' => [
271 | 'attempts' => 3,
272 | 'frequency' => 5 * 60, // 5 minutes
273 | 'period' => 30 * 60, // 30 minutes
274 | ],
275 | ],
276 |
277 | 'rfi' => [
278 | 'enabled' => env('FIREWALL_MIDDLEWARE_RFI_ENABLED', env('FIREWALL_ENABLED', true)),
279 |
280 | 'methods' => ['get', 'post', 'delete'],
281 |
282 | 'routes' => [
283 | 'only' => [], // i.e. 'contact'
284 | 'except' => [], // i.e. 'admin/*'
285 | ],
286 |
287 | 'inputs' => [
288 | 'only' => [], // i.e. 'first_name'
289 | 'except' => [], // i.e. 'password'
290 | ],
291 |
292 | 'patterns' => [
293 | '#(http|ftp){1,1}(s){0,1}://.*#i',
294 | ],
295 |
296 | 'exceptions' => [],
297 |
298 | 'auto_block' => [
299 | 'attempts' => 3,
300 | 'frequency' => 5 * 60, // 5 minutes
301 | 'period' => 30 * 60, // 30 minutes
302 | ],
303 | ],
304 |
305 | 'session' => [
306 | 'enabled' => env('FIREWALL_MIDDLEWARE_SESSION_ENABLED', env('FIREWALL_ENABLED', true)),
307 |
308 | 'methods' => ['get', 'post', 'delete'],
309 |
310 | 'routes' => [
311 | 'only' => [], // i.e. 'contact'
312 | 'except' => [], // i.e. 'admin/*'
313 | ],
314 |
315 | 'inputs' => [
316 | 'only' => [], // i.e. 'first_name'
317 | 'except' => [], // i.e. 'password'
318 | ],
319 |
320 | 'patterns' => [
321 | '@[\|:]O:\d{1,}:"[\w_][\w\d_]{0,}":\d{1,}:{@i',
322 | '@[\|:]a:\d{1,}:{@i',
323 | ],
324 |
325 | 'auto_block' => [
326 | 'attempts' => 3,
327 | 'frequency' => 5 * 60, // 5 minutes
328 | 'period' => 30 * 60, // 30 minutes
329 | ],
330 | ],
331 |
332 | 'sqli' => [
333 | 'enabled' => env('FIREWALL_MIDDLEWARE_SQLI_ENABLED', env('FIREWALL_ENABLED', true)),
334 |
335 | 'methods' => ['get', 'delete'],
336 |
337 | 'routes' => [
338 | 'only' => [], // i.e. 'contact'
339 | 'except' => [], // i.e. 'admin/*'
340 | ],
341 |
342 | 'inputs' => [
343 | 'only' => [], // i.e. 'first_name'
344 | 'except' => [], // i.e. 'password'
345 | ],
346 |
347 | 'patterns' => [
348 | '#[\d\W](union select|union join|union distinct)[\d\W]#is',
349 | '#[\d\W](union|union select|insert|from|where|concat|into|cast|truncate|select|delete|having)[\d\W]#is',
350 | ],
351 |
352 | 'auto_block' => [
353 | 'attempts' => 3,
354 | 'frequency' => 5 * 60, // 5 minutes
355 | 'period' => 30 * 60, // 30 minutes
356 | ],
357 | ],
358 |
359 | 'swear' => [
360 | 'enabled' => env('FIREWALL_MIDDLEWARE_SWEAR_ENABLED', env('FIREWALL_ENABLED', true)),
361 |
362 | 'methods' => ['post', 'put', 'patch'],
363 |
364 | 'routes' => [
365 | 'only' => [], // i.e. 'contact'
366 | 'except' => [], // i.e. 'admin/*'
367 | ],
368 |
369 | 'inputs' => [
370 | 'only' => [], // i.e. 'first_name'
371 | 'except' => [], // i.e. 'password'
372 | ],
373 |
374 | 'words' => [],
375 |
376 | 'auto_block' => [
377 | 'attempts' => 3,
378 | 'frequency' => 5 * 60, // 5 minutes
379 | 'period' => 30 * 60, // 30 minutes
380 | ],
381 | ],
382 |
383 | 'url' => [
384 | 'enabled' => env('FIREWALL_MIDDLEWARE_URL_ENABLED', env('FIREWALL_ENABLED', true)),
385 |
386 | 'methods' => ['all'],
387 |
388 | 'inspections' => [], // i.e. 'admin'
389 |
390 | 'auto_block' => [
391 | 'attempts' => 5,
392 | 'frequency' => 1 * 60, // 1 minute
393 | 'period' => 30 * 60, // 30 minutes
394 | ],
395 | ],
396 |
397 | 'whitelist' => [
398 | 'enabled' => env('FIREWALL_MIDDLEWARE_WHITELIST_ENABLED', env('FIREWALL_ENABLED', true)),
399 |
400 | 'methods' => ['all'],
401 |
402 | 'routes' => [
403 | 'only' => [], // i.e. 'contact'
404 | 'except' => [], // i.e. 'admin/*'
405 | ],
406 | ],
407 |
408 | 'xss' => [
409 | 'enabled' => env('FIREWALL_MIDDLEWARE_XSS_ENABLED', env('FIREWALL_ENABLED', true)),
410 |
411 | 'methods' => ['post', 'put', 'patch'],
412 |
413 | 'routes' => [
414 | 'only' => [], // i.e. 'contact'
415 | 'except' => [], // i.e. 'admin/*'
416 | ],
417 |
418 | 'inputs' => [
419 | 'only' => [], // i.e. 'first_name'
420 | 'except' => [], // i.e. 'password'
421 | ],
422 |
423 | 'patterns' => [
424 | // Evil starting attributes
425 | '#(<[^>]+[\x00-\x20\"\'\/])(form|formaction|on\w*|style|xmlns|xlink:href)[^>]*>?#iUu',
426 |
427 | // javascript:, livescript:, vbscript:, mocha: protocols
428 | '!((java|live|vb)script|mocha|feed|data):(\w)*!iUu',
429 | '#-moz-binding[\x00-\x20]*:#u',
430 |
431 | // Unneeded tags
432 | '#*(applet|meta|xml|blink|link|style|script|embed|object|iframe|frame|frameset|ilayer|layer|bgsound|title|base|img)[^>]*>?#i'
433 | ],
434 |
435 | 'auto_block' => [
436 | 'attempts' => 3,
437 | 'frequency' => 5 * 60, // 5 minutes
438 | 'period' => 30 * 60, // 30 minutes
439 | ],
440 | ],
441 |
442 | ],
443 |
444 | ];
445 |
--------------------------------------------------------------------------------
/src/Events/AttackDetected.php:
--------------------------------------------------------------------------------
1 | log = $log;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Exceptions/AccessDenied.php:
--------------------------------------------------------------------------------
1 | copy()->subSeconds(config('firewall.middleware.' . $event->log->middleware . '.auto_block.frequency'));
23 |
24 | $log = config('firewall.models.log', Log::class);
25 | $count = $log::where('ip', $event->log->ip)
26 | ->where('middleware', $event->log->middleware)
27 | ->whereBetween('created_at', [$start, $end])
28 | ->count();
29 |
30 | if ($count != config('firewall.middleware.' . $event->log->middleware . '.auto_block.attempts')) {
31 | return;
32 | }
33 |
34 | $ip = config('firewall.models.ip', Ip::class);
35 | $ip::create([
36 | 'ip' => $event->log->ip,
37 | 'log_id' => $event->log->id,
38 | ]);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Listeners/CheckLogin.php:
--------------------------------------------------------------------------------
1 | request = request();
16 | $this->middleware = 'login';
17 | $this->user_id = 0;
18 |
19 | if ($this->skip($event)) {
20 | return;
21 | }
22 |
23 | $this->request['password'] = '******';
24 |
25 | $log = $this->log();
26 |
27 | event(new AttackDetected($log));
28 | }
29 |
30 | public function skip($event): bool
31 | {
32 | if ($this->isDisabled()) {
33 | return true;
34 | }
35 |
36 | if ($this->isWhitelist()) {
37 | return true;
38 | }
39 |
40 | return false;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Listeners/NotifyUsers.php:
--------------------------------------------------------------------------------
1 | notify(new AttackDetected($event->log));
23 | } catch (Throwable $e) {
24 | report($e);
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Middleware/Agent.php:
--------------------------------------------------------------------------------
1 | parser = new Parser();
18 |
19 | if ($this->isMalicious()) {
20 | $status = true;
21 | }
22 |
23 | if (! $status && $this->isBrowser()) {
24 | $status = true;
25 | }
26 |
27 | if (! $status && $this->isPlatform()) {
28 | $status = true;
29 | }
30 |
31 | if (! $status && $this->isDevice()) {
32 | $status = true;
33 | }
34 |
35 | if (! $status && $this->isProperty()) {
36 | $status = true;
37 | }
38 |
39 | if ($status) {
40 | $log = $this->log();
41 |
42 | event(new AttackDetected($log));
43 | }
44 |
45 | return $status;
46 | }
47 |
48 | protected function isMalicious()
49 | {
50 | $agent = $this->parser->getUserAgent();
51 |
52 | if (empty($agent) || ($agent == '-') || strstr($agent, '')) {
53 | return true;
54 | }
55 |
56 | $patterns = [
57 | '@"feed_url@',
58 | '@}__(.*)|O:@',
59 | '@J?Simple(p|P)ie(Factory)?@',
60 | ];
61 |
62 | foreach ($patterns as $pattern) {
63 | if (! preg_match($pattern, $agent) == 1) {
64 | continue;
65 | }
66 |
67 | return true;
68 | }
69 |
70 | return false;
71 | }
72 |
73 | protected function isBrowser()
74 | {
75 | if (! $browsers = config('firewall.middleware.' . $this->middleware . '.browsers')) {
76 | return false;
77 | }
78 |
79 | if (! empty($browsers['allow']) && ! in_array((string) $this->parser->browser(), (array) $browsers['allow'])) {
80 | return true;
81 | }
82 |
83 | if (in_array((string) $this->parser->browser(), (array) $browsers['block'])) {
84 | return true;
85 | }
86 |
87 | return false;
88 | }
89 |
90 | protected function isPlatform()
91 | {
92 | if (! $platforms = config('firewall.middleware.' . $this->middleware . '.platforms')) {
93 | return false;
94 | }
95 |
96 | if (! empty($platforms['allow']) && ! in_array((string) $this->parser->platform(), (array) $platforms['allow'])) {
97 | return true;
98 | }
99 |
100 | if (in_array((string) $this->parser->platform(), (array) $platforms['block'])) {
101 | return true;
102 | }
103 |
104 | return false;
105 | }
106 |
107 | protected function isDevice()
108 | {
109 | if (! $devices = config('firewall.middleware.' . $this->middleware . '.devices')) {
110 | return false;
111 | }
112 |
113 | $list = ['Desktop', 'Mobile', 'Tablet'];
114 |
115 | foreach ((array) $devices['allow'] as $allow) {
116 | if (! in_array($allow, $list)) {
117 | continue;
118 | }
119 |
120 | $function = 'is' . ucfirst($allow);
121 |
122 | if ($this->parser->$function()) {
123 | continue;
124 | }
125 |
126 | return true;
127 | }
128 |
129 | foreach ((array) $devices['block'] as $block) {
130 | if (! in_array($block, $list)) {
131 | continue;
132 | }
133 |
134 | $function = 'is' . ucfirst($block);
135 |
136 | if (! $this->parser->$function()) {
137 | continue;
138 | }
139 |
140 | return true;
141 | }
142 |
143 | return false;
144 | }
145 |
146 | protected function isProperty()
147 | {
148 | if (! $agents = config('firewall.middleware.' . $this->middleware . '.properties')) {
149 | return false;
150 | }
151 |
152 | foreach ((array) $agents['allow'] as $allow) {
153 | if ($this->parser->is((string) $allow)) {
154 | continue;
155 | }
156 |
157 | return true;
158 | }
159 |
160 | foreach ((array) $agents['block'] as $block) {
161 | if (! $this->parser->is((string) $block)) {
162 | continue;
163 | }
164 |
165 | return true;
166 | }
167 |
168 | return false;
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/src/Middleware/Bot.php:
--------------------------------------------------------------------------------
1 | isRobot()) {
16 | return false;
17 | }
18 |
19 | if (! $crawlers = config('firewall.middleware.' . $this->middleware . '.crawlers')) {
20 | return false;
21 | }
22 |
23 | $status = false;
24 |
25 | if (! empty($crawlers['allow']) && ! in_array((string) $agent->robot(), (array) $crawlers['allow'])) {
26 | $status = true;
27 | }
28 |
29 | if (in_array((string) $agent->robot(), (array) $crawlers['block'])) {
30 | $status = true;
31 | }
32 |
33 | if ($status) {
34 | $log = $this->log();
35 |
36 | event(new AttackDetected($log));
37 | }
38 |
39 | return $status;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Middleware/Geo.php:
--------------------------------------------------------------------------------
1 | isEmpty($places)) {
15 | return false;
16 | }
17 |
18 | if (! $location = $this->getLocation()) {
19 | return false;
20 | }
21 |
22 | foreach ($places as $place) {
23 | if (! $this->isFiltered($location, $place)) {
24 | continue;
25 | }
26 |
27 | return true;
28 | }
29 |
30 | return false;
31 | }
32 |
33 | protected function isEmpty($places)
34 | {
35 | foreach ($places as $place) {
36 | if (! $list = config('firewall.middleware.' . $this->middleware . '.' . $place)) {
37 | continue;
38 | }
39 |
40 | if (empty($list['allow']) && empty($list['block'])) {
41 | continue;
42 | }
43 |
44 | return false;
45 | }
46 |
47 | return true;
48 | }
49 |
50 | protected function isFiltered($location, $place)
51 | {
52 | if (! $list = config('firewall.middleware.' . $this->middleware . '.' . $place)) {
53 | return false;
54 | }
55 |
56 | $s_place = Str::singular($place);
57 |
58 | if (! empty($list['allow']) && ! in_array((string) $location->$s_place, (array) $list['allow'])) {
59 | return true;
60 | }
61 |
62 | if (in_array((string) $location->$s_place, (array) $list['block'])) {
63 | return true;
64 | }
65 |
66 | return false;
67 | }
68 |
69 | protected function getLocation()
70 | {
71 | $location = new \stdClass();
72 | $location->continent = $location->country = $location->region = $location->city = null;
73 |
74 | $service = config('firewall.middleware.' . $this->middleware . '.service');
75 |
76 | return $this->$service($location);
77 | }
78 |
79 | protected function ipapi($location)
80 | {
81 | $response = $this->getResponse('http://ip-api.com/json/' . $this->ip() . '?fields=continent,country,regionName,city');
82 |
83 | if (!is_object($response) || empty($response->country) || empty($response->city)) {
84 | return false;
85 | }
86 |
87 | $location->continent = $response->continent;
88 | $location->country = $response->country;
89 | $location->region = $response->regionName;
90 | $location->city = $response->city;
91 |
92 | return $location;
93 | }
94 |
95 | protected function extremeiplookup($location)
96 | {
97 | $response = $this->getResponse('https://extreme-ip-lookup.com/json/' . $this->ip());
98 |
99 | if (!is_object($response) || empty($response->country) || empty($response->city)) {
100 | return false;
101 | }
102 |
103 | $location->continent = $response->continent;
104 | $location->country = $response->country;
105 | $location->region = $response->region;
106 | $location->city = $response->city;
107 |
108 | return $location;
109 | }
110 |
111 | protected function ipstack($location)
112 | {
113 | $response = $this->getResponse('https://api.ipstack.com/' . $this->ip() . '?access_key=' . env('IPSTACK_KEY'));
114 |
115 | if (!is_object($response) || empty($response->country_name) || empty($response->region_name)) {
116 | return false;
117 | }
118 |
119 | $location->continent = $response->continent_name;
120 | $location->country = $response->country_name;
121 | $location->region = $response->region_name;
122 | $location->city = $response->city;
123 |
124 | return $location;
125 | }
126 |
127 | protected function ipdata($location)
128 | {
129 | $response = $this->getResponse('https://api.ipdata.co/' . $this->ip() . '?api-key=' . env('IPSTACK_KEY'));
130 |
131 | if (! is_object($response) || empty($response->country_name) || empty($response->region_name)) {
132 | return false;
133 | }
134 |
135 | $location->continent = $response->continent_name;
136 | $location->country = $response->country_name;
137 | $location->region = $response->region_name;
138 | $location->city = $response->city;
139 |
140 | return $location;
141 | }
142 |
143 | protected function ipinfo($location)
144 | {
145 | $response = $this->getResponse('https://ipinfo.io/' . $this->ip() . '/geo?token=' . env('IPINFO_KEY'));
146 |
147 | if (! is_object($response) || empty($response->country) || empty($response->city)) {
148 | return false;
149 | }
150 |
151 | $location->country = $response->country;
152 | $location->region = $response->region;
153 | $location->city = $response->city;
154 |
155 | return $location;
156 | }
157 |
158 | public function ipregistry($location)
159 | {
160 | $url = 'https://api.ipregistry.co/' . $this->ip() . '?key=' . env('IPREGISTRY_KEY');
161 |
162 | $response = $this->getResponse($url);
163 |
164 | if (! is_object($response) || empty($response->location)) {
165 | return false;
166 | }
167 |
168 | $location->continent = $response->location->continent->name;
169 | $location->country = $response->location->country->name;
170 | $location->country_code = $response->location->country->code;
171 | $location->region = $response->location->region->name;
172 | $location->city = $response->location->city;
173 | $location->timezone = $response->time_zone->id;
174 | $location->currency_code = $response->currency->code;
175 |
176 | $location->is_eu = $response->location->in_eu;
177 |
178 | if (! empty($response->location->language->code)) {
179 | $location->language_code = $response->location->language->code . '-' . $response->location->country->code;
180 | }
181 |
182 | return $location;
183 | }
184 |
185 | public function ip2locationio($location)
186 | {
187 | $url = 'https://api.ip2location.io/?ip=' . $this->ip() . '&key=' . env('IP2LOCATIONIO_KEY');
188 |
189 | $response = $this->getResponse($url);
190 |
191 | if (! is_object($response) || empty($response->location)) {
192 | return false;
193 | }
194 |
195 | $location->country = $response->country_name;
196 | $location->country_code = $response->country_code;
197 | $location->region = $response->region_name;
198 | $location->city = $response->city_name;
199 | $location->latitude = $response->latitude;
200 | $location->longitude = $response->longitude;
201 | $location->zipcode = $response->zip_code;
202 | $location->timezone = $response->time_zone;
203 | $location->asn = $response->asn;
204 | $location->as = $response->as;
205 |
206 | return $location;
207 | }
208 |
209 | protected function getResponse($url)
210 | {
211 | try {
212 | $ch = curl_init();
213 | curl_setopt($ch, CURLOPT_URL, $url);
214 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
215 | curl_setopt($ch, CURLOPT_TIMEOUT, 3);
216 | curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
217 | $content = curl_exec($ch);
218 | curl_close($ch);
219 |
220 | $response = json_decode($content);
221 | } catch (\ErrorException $e) {
222 | $response = null;
223 | }
224 |
225 | return $response;
226 | }
227 | }
228 |
--------------------------------------------------------------------------------
/src/Middleware/Ip.php:
--------------------------------------------------------------------------------
1 | ip())->pluck('id')->first();
18 | } catch (QueryException $e) {
19 | // Base table or view not found
20 | //$status = ($e->getCode() == '42S02') ? false : true;
21 | }
22 |
23 | return $status;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Middleware/Lfi.php:
--------------------------------------------------------------------------------
1 | $value) {
22 | if (empty($value)) {
23 | continue;
24 | }
25 |
26 | if (is_array($value)) {
27 | if (! $result = $this->match($pattern, $value)) {
28 | continue;
29 | }
30 |
31 | break;
32 | }
33 |
34 | if (! $this->isInput($key)) {
35 | continue;
36 | }
37 |
38 | if (! $result = (stripos($value, $pattern) === 0)) {
39 | continue;
40 | }
41 |
42 | break;
43 | }
44 |
45 | return $result;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Middleware/Referrer.php:
--------------------------------------------------------------------------------
1 | middleware . '.blocked')) {
15 | return $status;
16 | }
17 |
18 | if (in_array((string) $this->request->server('HTTP_REFERER'), (array) $blocked)) {
19 | $status = true;
20 | }
21 |
22 | if ($status) {
23 | $log = $this->log();
24 |
25 | event(new AttackDetected($log));
26 | }
27 |
28 | return $status;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Middleware/Rfi.php:
--------------------------------------------------------------------------------
1 | applyExceptions($input))) {
19 | return false;
20 | }
21 |
22 | return $this->checkContent($result);
23 | }
24 |
25 | foreach ($input as $key => $value) {
26 | if (empty($value)) {
27 | continue;
28 | }
29 |
30 | if (is_array($value)) {
31 | if (! $result = $this->match($pattern, $value)) {
32 | continue;
33 | }
34 |
35 | break;
36 | }
37 |
38 | if (! $this->isInput($key)) {
39 | continue;
40 | }
41 |
42 | if (! $result = preg_match($pattern, $this->applyExceptions($value))) {
43 | continue;
44 | }
45 |
46 | if (! $this->checkContent($result)) {
47 | continue;
48 | }
49 |
50 | break;
51 | }
52 |
53 | return $result;
54 | }
55 |
56 | protected function applyExceptions($string)
57 | {
58 | $exceptions = config('firewall.middleware.' . $this->middleware . '.exceptions');
59 |
60 | $domain = $this->request->getHost();
61 |
62 | $exceptions[] = 'http://' . $domain;
63 | $exceptions[] = 'https://' . $domain;
64 | $exceptions[] = 'http://&';
65 | $exceptions[] = 'https://&';
66 |
67 | return str_replace($exceptions, '', $string);
68 | }
69 |
70 | protected function checkContent($value)
71 | {
72 | $contents = @file_get_contents($value);
73 |
74 | if (!empty($contents)) {
75 | return (strstr($contents, 'middleware . '.words')) {
14 | return $patterns;
15 | }
16 |
17 | foreach ((array) $words as $word) {
18 | $patterns[] = '#\b' . $word . '\b#i';
19 | }
20 |
21 | return $patterns;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Middleware/Url.php:
--------------------------------------------------------------------------------
1 | middleware . '.inspections')) {
15 | return $protected;
16 | }
17 |
18 | foreach ($inspections as $inspection) {
19 | if (! $this->request->is($inspection)) {
20 | continue;
21 | }
22 |
23 | $protected = true;
24 |
25 | break;
26 | }
27 |
28 | if ($protected) {
29 | $log = $this->log();
30 |
31 | event(new AttackDetected($log));
32 | }
33 |
34 | return $protected;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Middleware/Whitelist.php:
--------------------------------------------------------------------------------
1 | isWhitelist() === false);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/Middleware/Xss.php:
--------------------------------------------------------------------------------
1 | increments('id');
17 | $table->string('ip');
18 | $table->integer('log_id')->nullable();
19 | $table->boolean('blocked')->default(1);
20 | $table->timestamps();
21 | $table->softDeletes();
22 |
23 | $table->index('ip');
24 | });
25 | }
26 |
27 | /**
28 | * Reverse the migrations.
29 | *
30 | * @return void
31 | */
32 | public function down()
33 | {
34 | Schema::drop('firewall_ips');
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Migrations/2019_07_15_000000_create_firewall_logs_table.php:
--------------------------------------------------------------------------------
1 | increments('id');
17 | $table->string('ip');
18 | $table->string('level')->default('medium');
19 | $table->string('middleware');
20 | $table->integer('user_id')->nullable();
21 | $table->text('url')->nullable();
22 | $table->string('referrer')->nullable();
23 | $table->text('request')->nullable();
24 | $table->timestamps();
25 | $table->softDeletes();
26 |
27 | $table->index('ip');
28 | });
29 | }
30 |
31 | /**
32 | * Reverse the migrations.
33 | *
34 | * @return void
35 | */
36 | public function down()
37 | {
38 | Schema::drop('firewall_logs');
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Models/Ip.php:
--------------------------------------------------------------------------------
1 | 'datetime',
18 | ];
19 |
20 | public function log()
21 | {
22 | return $this->belongsTo('Akaunting\Firewall\Models\Log');
23 | }
24 |
25 | public function logs()
26 | {
27 | return $this->hasMany('Akaunting\Firewall\Models\Log', 'ip', 'ip');
28 | }
29 |
30 | public function scopeBlocked($query, $ip = null)
31 | {
32 | $q = $query->where('blocked', 1);
33 |
34 | if ($ip) {
35 | $q = $query->where('ip', $ip);
36 | }
37 |
38 | return $q;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Models/Log.php:
--------------------------------------------------------------------------------
1 | 'datetime',
18 | ];
19 |
20 | public function user()
21 | {
22 | return $this->belongsTo(config('firewall.models.user'));
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Notifications/AttackDetected.php:
--------------------------------------------------------------------------------
1 | log = $log;
35 | $this->notifications = config('firewall.middleware.' . $log->middleware . '.notifications', config('firewall.notifications'));
36 | }
37 |
38 | /**
39 | * Get the notification's channels.
40 | *
41 | * @param mixed $notifiable
42 | * @return array|string
43 | */
44 | public function via($notifiable)
45 | {
46 | $channels = [];
47 |
48 | foreach ($this->notifications as $channel => $settings) {
49 | if (empty($settings['enabled'])) {
50 | continue;
51 | }
52 |
53 | $channels[] = $channel;
54 | }
55 |
56 | return $channels;
57 | }
58 |
59 | /**
60 | * Get the notification's queues.
61 | * @return array|string
62 | */
63 |
64 | public function viaQueues(): array
65 | {
66 | return array_map(fn ($channel) => $channel['queue'] ?? 'default', $this->notifications);
67 | }
68 |
69 | /**
70 | * Build the mail representation of the notification.
71 | *
72 | * @param mixed $notifiable
73 | * @return \Illuminate\Notifications\Messages\MailMessage
74 | */
75 | public function toMail($notifiable)
76 | {
77 | $domain = request()->getHttpHost();
78 |
79 | $subject = trans('firewall::notifications.mail.subject', [
80 | 'domain' => $domain,
81 | ]);
82 |
83 | $message = trans('firewall::notifications.mail.message', [
84 | 'domain' => $domain,
85 | 'middleware' => ucfirst($this->log->middleware),
86 | 'ip' => $this->log->ip,
87 | 'url' => $this->log->url,
88 | ]);
89 |
90 | return (new MailMessage)
91 | ->from($this->notifications['mail']['from'], $this->notifications['mail']['name'])
92 | ->subject($subject)
93 | ->line($message);
94 | }
95 |
96 | /**
97 | * Get the Slack representation of the notification.
98 | *
99 | * @param mixed $notifiable
100 | * @return SlackMessage
101 | */
102 | public function toSlack($notifiable)
103 | {
104 | $message = trans('firewall::notifications.slack.message', [
105 | 'domain' => request()->getHttpHost(),
106 | ]);
107 |
108 | return (new SlackMessage)
109 | ->error()
110 | ->from($this->notifications['slack']['from'], $this->notifications['slack']['emoji'])
111 | ->to($this->notifications['slack']['channel'])
112 | ->content($message)
113 | ->attachment(function ($attachment) {
114 | $attachment->fields([
115 | 'IP' => $this->log->ip,
116 | 'Type' => ucfirst($this->log->middleware),
117 | 'User ID' => $this->log->user_id,
118 | 'URL' => $this->log->url,
119 | ]);
120 | });
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/Notifications/Notifiable.php:
--------------------------------------------------------------------------------
1 | publishes([
33 | __DIR__ . '/Config/firewall.php' => config_path('firewall.php'),
34 | __DIR__ . '/Migrations/2019_07_15_000000_create_firewall_ips_table.php' => database_path('migrations/2019_07_15_000000_create_firewall_ips_table.php'),
35 | __DIR__ . '/Migrations/2019_07_15_000000_create_firewall_logs_table.php' => database_path('migrations/2019_07_15_000000_create_firewall_logs_table.php'),
36 | __DIR__ . '/Resources/lang' => $langPath,
37 | ], 'firewall');
38 |
39 | $this->registerMiddleware($router);
40 | $this->registerListeners();
41 | $this->registerTranslations($langPath);
42 | $this->registerCommands();
43 | }
44 |
45 | /**
46 | * Register the application services.
47 | *
48 | * @return void
49 | */
50 | public function register()
51 | {
52 | $this->mergeConfigFrom(__DIR__ . '/Config/firewall.php', 'firewall');
53 |
54 | $this->app->register(\Jenssegers\Agent\AgentServiceProvider::class);
55 | }
56 |
57 | /**
58 | * Register middleware.
59 | *
60 | * @param Router $router
61 | *
62 | * @return void
63 | */
64 | public function registerMiddleware($router)
65 | {
66 | $router->middlewareGroup('firewall.all', config('firewall.all_middleware'));
67 | $router->aliasMiddleware('firewall.agent', 'Akaunting\Firewall\Middleware\Agent');
68 | $router->aliasMiddleware('firewall.bot', 'Akaunting\Firewall\Middleware\Bot');
69 | $router->aliasMiddleware('firewall.ip', 'Akaunting\Firewall\Middleware\Ip');
70 | $router->aliasMiddleware('firewall.geo', 'Akaunting\Firewall\Middleware\Geo');
71 | $router->aliasMiddleware('firewall.lfi', 'Akaunting\Firewall\Middleware\Lfi');
72 | $router->aliasMiddleware('firewall.php', 'Akaunting\Firewall\Middleware\Php');
73 | $router->aliasMiddleware('firewall.referrer', 'Akaunting\Firewall\Middleware\Referrer');
74 | $router->aliasMiddleware('firewall.rfi', 'Akaunting\Firewall\Middleware\Rfi');
75 | $router->aliasMiddleware('firewall.session', 'Akaunting\Firewall\Middleware\Session');
76 | $router->aliasMiddleware('firewall.sqli', 'Akaunting\Firewall\Middleware\Sqli');
77 | $router->aliasMiddleware('firewall.swear', 'Akaunting\Firewall\Middleware\Swear');
78 | $router->aliasMiddleware('firewall.url', 'Akaunting\Firewall\Middleware\Url');
79 | $router->aliasMiddleware('firewall.whitelist', 'Akaunting\Firewall\Middleware\Whitelist');
80 | $router->aliasMiddleware('firewall.xss', 'Akaunting\Firewall\Middleware\Xss');
81 | }
82 |
83 | /**
84 | * Register listeners.
85 | *
86 | * @return void
87 | */
88 | public function registerListeners()
89 | {
90 | $this->app['events']->listen(AttackDetected::class, BlockIp::class);
91 | $this->app['events']->listen(AttackDetected::class, NotifyUsers::class);
92 | $this->app['events']->listen(LoginFailed::class, CheckLogin::class);
93 | }
94 |
95 | /**
96 | * Register translations.
97 | *
98 | * @return void
99 | */
100 | public function registerTranslations($langPath)
101 | {
102 | $this->loadTranslationsFrom(__DIR__ . '/Resources/lang', 'firewall');
103 |
104 | $this->loadTranslationsFrom($langPath, 'firewall');
105 | }
106 |
107 | public function registerCommands()
108 | {
109 | $this->commands(UnblockIp::class);
110 |
111 | if (config('firewall.cron.enabled')) {
112 | $this->app->booted(function () {
113 | app(Schedule::class)->command('firewall:unblockip')->cron(config('firewall.cron.expression'));
114 | });
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/Resources/lang/en/notifications.php:
--------------------------------------------------------------------------------
1 | [
6 |
7 | 'subject' => '🔥 Possible attack on :domain',
8 | 'message' => 'A possible :middleware attack on :domain has been detected from :ip address. The following URL has been affected: :url',
9 |
10 | ],
11 |
12 | 'slack' => [
13 |
14 | 'message' => 'A possible attack on :domain has been detected.',
15 |
16 | ],
17 |
18 | ];
19 |
--------------------------------------------------------------------------------
/src/Resources/lang/en/responses.php:
--------------------------------------------------------------------------------
1 | [
6 |
7 | 'message' => 'Access Denied',
8 |
9 | ],
10 |
11 | ];
12 |
--------------------------------------------------------------------------------
/src/Traits/Helper.php:
--------------------------------------------------------------------------------
1 | middleware;
18 |
19 | return config('firewall.middleware.' . $middleware . '.enabled', config('firewall.enabled'));
20 | }
21 |
22 | public function isDisabled($middleware = null)
23 | {
24 | return ! $this->isEnabled($middleware);
25 | }
26 |
27 | public function isWhitelist()
28 | {
29 | return IpUtils::checkIp($this->ip(), config('firewall.whitelist'));
30 | }
31 |
32 | public function isMethod($middleware = null)
33 | {
34 | $middleware = $middleware ?? $this->middleware;
35 |
36 | if (! $methods = config('firewall.middleware.' . $middleware . '.methods')) {
37 | return false;
38 | }
39 |
40 | if (in_array('all', $methods)) {
41 | return true;
42 | }
43 |
44 | return in_array(strtolower($this->request->method()), $methods);
45 | }
46 |
47 | public function isRoute($middleware = null)
48 | {
49 | $middleware = $middleware ?? $this->middleware;
50 |
51 | if (! $routes = config('firewall.middleware.' . $middleware . '.routes')) {
52 | return false;
53 | }
54 |
55 | foreach ($routes['except'] as $ex) {
56 | if (! $this->request->is($ex)) {
57 | continue;
58 | }
59 |
60 | return true;
61 | }
62 |
63 | foreach ($routes['only'] as $on) {
64 | if ($this->request->is($on)) {
65 | continue;
66 | }
67 |
68 | return true;
69 | }
70 |
71 | return false;
72 | }
73 |
74 | public function isInput($name, $middleware = null)
75 | {
76 | $middleware = $middleware ?? $this->middleware;
77 |
78 | if (! $inputs = config('firewall.middleware.' . $middleware . '.inputs')) {
79 | return true;
80 | }
81 |
82 | if (! empty($inputs['only']) && ! in_array((string) $name, (array) $inputs['only'])) {
83 | return false;
84 | }
85 |
86 | return ! in_array((string) $name, (array) $inputs['except']);
87 | }
88 |
89 | public function log($middleware = null, $user_id = null, $level = 'medium')
90 | {
91 | $middleware = $middleware ?? $this->middleware;
92 | $user_id = $user_id ?? $this->user_id;
93 |
94 | $model = config('firewall.models.log', Log::class);
95 |
96 | $input = urldecode(http_build_query($this->request->input()));
97 |
98 | return $model::create([
99 | 'ip' => $this->ip(),
100 | 'level' => $level,
101 | 'middleware' => $middleware,
102 | 'user_id' => $user_id,
103 | 'url' => $this->request->fullUrl(),
104 | 'referrer' => substr($this->request->server('HTTP_REFERER'), 0, 191) ?: 'NULL',
105 | 'request' => substr($input, 0, config('firewall.log.max_request_size')),
106 | ]);
107 | }
108 |
109 | public function ip()
110 | {
111 | if ($cf_ip = $this->request->header('CF_CONNECTING_IP')) {
112 | $ip = $cf_ip;
113 | } else {
114 | $ip = $this->request->ip();
115 | }
116 |
117 | return $ip;
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/tests/Feature/IpTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('next', (new Ip())->handle($this->app->request, $this->getNextClosure()));
14 | }
15 |
16 | public function testShouldBlock()
17 | {
18 | Model::create(['ip' => '127.0.0.1', 'log_id' => 1]);
19 |
20 | $this->assertEquals('403', (new Ip())->handle($this->app->request, $this->getNextClosure())->getStatusCode());
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/Feature/LfiTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('next', (new Lfi())->handle($this->app->request, $this->getNextClosure()));
13 | }
14 |
15 | public function testShouldBlock()
16 | {
17 | $this->app->request->query->set('foo', '../../../../etc/passwd');
18 |
19 | $this->assertEquals('403', (new Lfi())->handle($this->app->request, $this->getNextClosure())->getStatusCode());
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/Feature/RfiTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('next', (new Rfi())->handle($this->app->request, $this->getNextClosure()));
13 | }
14 |
15 | public function testShouldBlock()
16 | {
17 | $this->app->request->query->set('foo', 'https://attacker.example.com/evil.php');
18 |
19 | $this->assertEquals('403', (new Rfi())->handle($this->app->request, $this->getNextClosure())->getStatusCode());
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/Feature/SqliTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('next', (new Sqli())->handle($this->app->request, $this->getNextClosure()));
13 | }
14 |
15 | public function testShouldBlock()
16 | {
17 | $this->app->request->query->set('foo', '-1+union+select+1,2,3,4,5,6,7,8,9,(SELECT+password+FROM+users+WHERE+ID=1)');
18 |
19 | $this->assertEquals('403', (new Sqli())->handle($this->app->request, $this->getNextClosure())->getStatusCode());
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/Feature/WhitelistTest.php:
--------------------------------------------------------------------------------
1 | ['127.0.0.0/24']]);
13 |
14 | $this->assertEquals('next', (new Whitelist())->handle($this->app->request, $this->getNextClosure()));
15 | }
16 |
17 | public function testShouldAllowMultiple()
18 | {
19 | config(['firewall.whitelist' => ['127.0.0.0/24', '127.0.0.1']]);
20 |
21 | $this->assertEquals('next', (new Whitelist())->handle($this->app->request, $this->getNextClosure()));
22 | }
23 |
24 | public function testShouldBlock()
25 | {
26 | $this->assertEquals('403', (new Whitelist())->handle($this->app->request, $this->getNextClosure())->getStatusCode());
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tests/Feature/XssTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('next', (new Xss())->handle($this->app->request, $this->getNextClosure()));
13 | }
14 |
15 | public function testShouldBlock()
16 | {
17 | $this->app->request->query->set('foo', '');
18 |
19 | $this->assertEquals('403', (new Xss())->handle($this->app->request, $this->getNextClosure())->getStatusCode());
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | setUpDatabase();
15 |
16 | $this->setUpConfig();
17 |
18 | $this->artisan('vendor:publish', ['--tag' => 'firewall']);
19 | $this->artisan('migrate:refresh', ['--database' => 'testbench']);
20 | }
21 |
22 | protected function tearDown(): void
23 | {
24 | parent::tearDown();
25 | }
26 |
27 | protected function getPackageProviders($app)
28 | {
29 | return [
30 | Provider::class,
31 | ];
32 | }
33 |
34 | protected function setUpDatabase()
35 | {
36 | config(['database.default' => 'testbench']);
37 |
38 | config(['database.connections.testbench' => [
39 | 'driver' => 'sqlite',
40 | 'database' => ':memory:',
41 | 'prefix' => '',
42 | ],
43 | ]);
44 | }
45 |
46 | protected function setUpConfig()
47 | {
48 | config(['firewall' => require __DIR__ . '/../src/Config/firewall.php']);
49 |
50 | config(['firewall.notifications.mail.enabled' => false]);
51 | config(['firewall.middleware.ip.methods' => ['all']]);
52 | config(['firewall.middleware.lfi.methods' => ['all']]);
53 | config(['firewall.middleware.rfi.methods' => ['all']]);
54 | config(['firewall.middleware.sqli.methods' => ['all']]);
55 | config(['firewall.middleware.xss.methods' => ['all']]);
56 | }
57 |
58 | public function getNextClosure()
59 | {
60 | return function () {
61 | return 'next';
62 | };
63 | }
64 | }
65 |
--------------------------------------------------------------------------------