├── .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 | ![Downloads](https://img.shields.io/packagist/dt/akaunting/laravel-firewall) 4 | ![Tests](https://img.shields.io/github/actions/workflow/status/akaunting/laravel-firewall/tests.yml?label=tests) 5 | [![StyleCI](https://github.styleci.io/repos/197242392/shield?style=flat&branch=master)](https://styleci.io/repos/197242392) 6 | [![License](https://img.shields.io/github/license/akaunting/laravel-firewall)](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 | '#]*>?#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, '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 | --------------------------------------------------------------------------------