Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.1, 8.2, 8.3]
php: [8.1, 8.2, 8.3, 8.4]
stability: [prefer-stable]

name: PHP ${{ matrix.php }} - ${{ matrix.stability }}

steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v3

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand All @@ -33,4 +33,4 @@ jobs:
run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress

- name: Execute tests
run: vendor/bin/phpunit
run: vendor/bin/pest
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.PHONY: test

install:
composer update

test:
./vendor/bin/pest
49 changes: 31 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,10 @@

### Configure once and forget about XSS attacks!

Laravel 5.4+ Middleware to filter user inputs from XSS and iframes and other embed elements.

It does not remove the html, it is only escaped script tags and embeds.

It does not remove the html, it is only escaped script tags and embeds.
However, by default, it does delete inline event listeners such as `onclick`.
Optionally they also can be escaped (set `escape_inline_listeners` to `true` in `xss-filter.php` config file).


For example

```php
Expand Down Expand Up @@ -84,27 +80,44 @@ From command line
composer require masterro/laravel-xss-filter
```

## Step 2: register Service provider and Facade(optional) (for Laravel 5.4)
For your Laravel app, open `config/app.php` and, within the `providers` array, append:

```php
MasterRO\LaravelXSSFilter\XSSFilterServiceProvider::class
```
within the `aliases` array, append:
```php
'XSSCleaner' => MasterRO\LaravelXSSFilter\XSSCleanerFacade::class
```

## Step 3: publish configs (optional)
## Step 2: publish configs (optional)
From command line
```
php artisan vendor:publish --provider="MasterRO\LaravelXSSFilter\XSSFilterServiceProvider"
```

## Step 4: Middleware
## Step 3: Middleware
You can register `\MasterRO\LaravelXSSFilter\FilterXSS::class` for filtering in global middleware stack, group middleware stack or for specific routes.
> Have a look at [Laravel's middleware documentation](https://laravel.com/docs/middleware#registering-middleware), if you need any help.

### Livewire
If you are using Livewire you can either register global middleware to all the update livewire requests. This special middleware will clean only required part of Livewire request payload and will not touch snapshot so the component checksum still would be valid.
```php
// AppServiceProvider.php

public function boot(): void
{
Livewire::setUpdateRoute(static function ($handle) {
return Route::post('/livewire/update', $handle)
->middleware(['web', FilterXSSLivewire::class]);
});
}
```

Or you can apply middleware to specific routes and add it to persistent list to ensure inputs are cleared on subsequent component requests:
```php
// AppServiceProvider.php

public function boot(): void
{
Livewire::addPersistentMiddleware([
FilterXSSLivewire::class,
]);
}
```

NOTE! If you have both Livewire components and traditional Controllers you can apply only `FilterXSSLivewire::class` middleware for all required routes or globally. It will fall back to base logic for non Livewire requests.

# Usage
After adding middleware, every request will be filtered.

Expand Down
14 changes: 10 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
}
],
"require": {
"php": ">=7.4",
"laravel/framework": "^6.20.26|^7.30.6|^8.0|^9.0|^10.0|^11.0"
"php": ">=8.1",
"laravel/framework": "^8.0|^9.0|^10.0|^11.0"
},
"require-dev": {
"orchestra/testbench": "^v4.0|^v5.0|^v6.0|^v7.0|^8.0|^9.0"
"orchestra/testbench": "^v6.0|^v7.0|^8.0|^9.0",
"pestphp/pest": "^2.36"
},
"autoload": {
"psr-4": {
Expand All @@ -34,8 +35,13 @@
"MasterRO\\LaravelXSSFilter\\XSSFilterServiceProvider"
],
"aliases": {
"XSSCleaner": "MasterRO\\LaravelXSSFilter\\XSSCleanerFacade"
"XSSCleaner": "MasterRO\\LaravelXSSFilter\\Facade\\XSSCleaner"
}
}
},
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true
}
}
}
File renamed without changes.
23 changes: 10 additions & 13 deletions src/Cleaner.php → src/Cleaner/Cleaner.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,16 @@

declare(strict_types=1);

namespace MasterRO\LaravelXSSFilter;
namespace MasterRO\LaravelXSSFilter\Cleaner;

use Illuminate\Support\Arr;
use Illuminate\Support\Str;

class Cleaner
{
protected CleanerConfig $config;

public function __construct(CleanerConfig $config)
{
$this->config = $config;
}
public function __construct(
protected CleanerConfig $config,
) {}

public function withConfig(CleanerConfig $config): Cleaner
{
Expand Down Expand Up @@ -51,15 +48,15 @@ public function escapeElements(string $value): string

public function cleanMediaElements(string $value): string
{
if (! $this->config->allowedMediaHosts()) {
if (!$this->config->allowedMediaHosts()) {
return $value;
}

$allowedUrls = collect($this->config->allowedMediaHosts())
->map(
fn(string $host) => ! Str::startsWith($host, ['http', 'https', '//'])
fn(string $host) => !Str::startsWith($host, ['http', 'https', '//'])
? ["http://{$host}", "https://{$host}", "//{$host}"]
: [$host]
: [$host],
)
->flatten()
->all();
Expand All @@ -72,7 +69,7 @@ public function cleanMediaElements(string $value): string
$urls = Arr::get($sources, '1', []);

foreach ($urls as $url) {
if (! Str::startsWith($url, $allowedUrls)) {
if (!Str::startsWith($url, $allowedUrls)) {
$value = str_replace($url, '#!', $value);
}
}
Expand All @@ -87,7 +84,7 @@ public function removeInlineEventListeners(string $value): string
$value = preg_replace($pattern, '', $value);
}

return ! is_string($value) ? '' : $value;
return !is_string($value) ? '' : $value;
}

public function escapeInlineEventListeners(string $value): string
Expand All @@ -96,7 +93,7 @@ public function escapeInlineEventListeners(string $value): string
$value = preg_replace_callback($pattern, [$this, 'escapeEqualSign'], $value);
}

return ! is_string($value) ? '' : $value;
return !is_string($value) ? '' : $value;
}

protected function escapeEqualSign(array $matches): string
Expand Down
2 changes: 1 addition & 1 deletion src/CleanerConfig.php → src/Cleaner/CleanerConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace MasterRO\LaravelXSSFilter;
namespace MasterRO\LaravelXSSFilter\Cleaner;

use Illuminate\Support\Str;

Expand Down
16 changes: 16 additions & 0 deletions src/Facade/XSSCleaner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace MasterRO\LaravelXSSFilter\Facade;

use Illuminate\Support\Facades\Facade;
use MasterRO\LaravelXSSFilter\Cleaner\Cleaner;

class XSSCleaner extends Facade
{
protected static function getFacadeAccessor(): string
{
return Cleaner::class;
}
}
31 changes: 8 additions & 23 deletions src/FilterXSS.php → src/Middleware/FilterXSS.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,24 @@

declare(strict_types=1);

namespace MasterRO\LaravelXSSFilter;
namespace MasterRO\LaravelXSSFilter\Middleware;

use Illuminate\Foundation\Http\Middleware\TransformsRequest;
use MasterRO\LaravelXSSFilter\Cleaner\Cleaner;

/**
* Class FilterXSS
*
* @package MasterRO\LaravelXSSFilter
*/
class FilterXSS extends TransformsRequest
{
/**
* The attributes that should not be filtered.
*
* @var array
*/
protected $except = [];
protected array $except = [];

/**
* @var Cleaner
*/
protected $cleaner;

/**
* FilterXSS constructor.
*
* @param Cleaner $cleaner
*/
public function __construct(Cleaner $cleaner)
{
public function __construct(
protected Cleaner $cleaner,
) {
$this->except = config('xss-filter.except', []);
$this->cleaner = $cleaner;
}

/**
Expand All @@ -44,17 +30,16 @@ public function __construct(Cleaner $cleaner)
*
* @return string|mixed
*/
protected function transform($key, $value)
protected function transform($key, $value): mixed
{
if (in_array($key, $this->except, true)) {
return $value;
}

if (! is_string($value)) {
if (!is_string($value)) {
return $value;
}

return $this->cleaner->clean($value);
}

}
46 changes: 46 additions & 0 deletions src/Middleware/FilterXSSLivewire.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace MasterRO\LaravelXSSFilter\Middleware;

use Closure;
use Illuminate\Http\Request;

class FilterXSSLivewire extends FilterXSS
{
public function handle($request, Closure $next)
{
if (! $this->isLivewireRequest($request)) {
return $next($request);
}

$this->cleanLivewirePayload($request);

return $next($request);
}

protected function cleanLivewirePayload(Request $request): void
{
$components = $request->input('components');

foreach ($components as $i => &$component) {
if (isset($component['updates'])) {
$component['updates'] = $this->cleanArray($component['updates'], "components.{$i}.updates.");
}

if (isset($component['calls'])) {
foreach ($component['calls'] as $j => &$call) {
$call['params'] = $this->cleanArray($call['params'], "components.{$i}.calls.{$j}.params.");
}
}
}

$request->request->set('components', $components);
}

protected function isLivewireRequest(Request $request): bool
{
return $request->routeIs('*livewire.update');
}
}
21 changes: 0 additions & 21 deletions src/XSSCleanerFacade.php

This file was deleted.

Loading