├── .github └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── composer.json ├── config └── xss-filter.php ├── phpunit.xml ├── src ├── Cleaner │ ├── Cleaner.php │ └── CleanerConfig.php ├── Facade │ └── XSSCleaner.php ├── Middleware │ ├── FilterXSS.php │ └── FilterXSSLivewire.php └── XSSFilterServiceProvider.php └── tests └── FilterXSSTest.php /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * *' 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | fail-fast: true 15 | matrix: 16 | php: [8.1, 8.2, 8.3, 8.4] 17 | stability: [prefer-stable] 18 | 19 | name: PHP ${{ matrix.php }} - ${{ matrix.stability }} 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v3 24 | 25 | - name: Setup PHP 26 | uses: shivammathur/setup-php@v2 27 | with: 28 | php-version: ${{ matrix.php }} 29 | extensions: dom, curl, libxml, mbstring, zip 30 | coverage: none 31 | 32 | - name: Install dependencies 33 | run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress 34 | 35 | - name: Execute tests 36 | run: vendor/bin/pest 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /vendor/ 3 | composer.lock 4 | .phpunit.result.cache 5 | phpunit.xml.bak 6 | .phpunit.cache 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Roman Ihoshyn 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. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | install: 4 | composer update 5 | 6 | test: 7 | ./vendor/bin/pest 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | 7 | Latest Stable Version 8 | 9 | 10 | Total Downloads 11 | 12 | 13 | Build Status 14 | 15 | 16 | License 17 | 18 |

19 | 20 |

21 | 22 | StandWithUkraine 23 | 24 |

25 | 26 | # XSS Filter/Sanitizer for Laravel 27 | 28 | ### Configure once and forget about XSS attacks! 29 | 30 | It does not remove the html, it is only escaped script tags and embeds. 31 | However, by default, it does delete inline event listeners such as `onclick`. 32 | Optionally they also can be escaped (set `escape_inline_listeners` to `true` in `xss-filter.php` config file). 33 | 34 | For example 35 | 36 | ```php 37 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | 47 |

Aawfawfaw f awf aw

48 | 49 | 50 | 51 | ``` 52 | 53 | will be transformed to 54 | 55 | ```php 56 | 57 | 58 | <script src="app.js"></script> 59 | <script>window.init()</script> 60 | 61 | <script> 62 | let Iframe = new Iframe('#iframe'); 63 | </script> 64 | 65 | 66 |

Aawfawfaw f awf aw

67 | <iframe id="iframe">Not supported!</iframe> 68 | 69 | 70 | 71 | ``` 72 | 73 | This allows to render html in views based on users' input and don't be afraid of XSS attacks and embed elements. 74 | 75 | # Installation 76 | 77 | ## Step 1: Composer 78 | From command line 79 | ``` 80 | composer require masterro/laravel-xss-filter 81 | ``` 82 | 83 | ## Step 2: publish configs (optional) 84 | From command line 85 | ``` 86 | php artisan vendor:publish --provider="MasterRO\LaravelXSSFilter\XSSFilterServiceProvider" 87 | ``` 88 | 89 | ## Step 3: Middleware 90 | You can register `\MasterRO\LaravelXSSFilter\FilterXSS::class` for filtering in global middleware stack, group middleware stack or for specific routes. 91 | > Have a look at [Laravel's middleware documentation](https://laravel.com/docs/middleware#registering-middleware), if you need any help. 92 | 93 | ### Livewire 94 | 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. 95 | ```php 96 | // AppServiceProvider.php 97 | 98 | public function boot(): void 99 | { 100 | Livewire::setUpdateRoute(static function ($handle) { 101 | return Route::post('/livewire/update', $handle) 102 | ->middleware(['web', FilterXSSLivewire::class]); 103 | }); 104 | } 105 | ``` 106 | 107 | Or you can apply middleware to specific routes and add it to persistent list to ensure inputs are cleared on subsequent component requests: 108 | ```php 109 | // AppServiceProvider.php 110 | 111 | public function boot(): void 112 | { 113 | Livewire::addPersistentMiddleware([ 114 | FilterXSSLivewire::class, 115 | ]); 116 | } 117 | ``` 118 | 119 | 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. 120 | 121 | # Usage 122 | After adding middleware, every request will be filtered. 123 | 124 | If you need to specify attributes that should not be filtered add them to `xss-filter.except` config. By default, filter excepts `password` and `password_confirmation` fields. 125 | 126 | If you want to clean some value in other place (i.e. Controller) you can use `XSSCleaner` Facade. 127 | 128 | ```php 129 | $clean = XSSCleaner::clean($string); 130 | ``` 131 | 132 | #### Runtime configuration 133 | 134 | 135 | ```php 136 | XSSCleaner::config() 137 | ->allowElement('iframe') 138 | ->allowMediaHosts(['youtube.com', 'youtu.be']) 139 | ->denyElement('a'); 140 | 141 | $clean = XSSCleaner::clean($string); 142 | ``` 143 | 144 | 145 | #### _I will be grateful if you star this project :)_ 146 | 147 | 148 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "masterro/laravel-xss-filter", 3 | "description": "Filter user input for XSS but don't touch other html", 4 | "license": "MIT", 5 | "keywords": [ 6 | "laravel", 7 | "XSS", 8 | "middleware" 9 | ], 10 | "authors": [ 11 | { 12 | "name": "Roman Ihoshyn", 13 | "email": "igoshin18@gmail.com" 14 | } 15 | ], 16 | "require": { 17 | "php": ">=8.1", 18 | "laravel/framework": "^8.0|^9.0|^10.0|^11.0|^12.0" 19 | }, 20 | "require-dev": { 21 | "orchestra/testbench": "^v6.0|^v7.0|^8.0|^9.0|^10.0", 22 | "pestphp/pest": "^2.36|^3.7" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "MasterRO\\LaravelXSSFilter\\": "src/", 27 | "MasterRO\\LaravelXSSFilter\\Tests\\": "tests/" 28 | } 29 | }, 30 | "prefer-stable": true, 31 | "minimum-stability": "dev", 32 | "extra": { 33 | "laravel": { 34 | "providers": [ 35 | "MasterRO\\LaravelXSSFilter\\XSSFilterServiceProvider" 36 | ], 37 | "aliases": { 38 | "XSSCleaner": "MasterRO\\LaravelXSSFilter\\Facade\\XSSCleaner" 39 | } 40 | } 41 | }, 42 | "config": { 43 | "allow-plugins": { 44 | "pestphp/pest-plugin": true 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /config/xss-filter.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'password', 6 | 'password_confirmation', 7 | ], 8 | 9 | // If this value set to `true` inline listeners will be escaped, otherwise they will be removed. 10 | 'escape_inline_listeners' => false, 11 | 12 | // By default, all elements allowed. 13 | 'allowed_elements' => null, 14 | 15 | // Elements would be escaped (will be filtered out by allowed_elements). 16 | 'blocked_elements' => ['script', 'frame', 'iframe', 'object', 'embed'], 17 | 18 | 'media_elements' => ['img', 'audio', 'video', 'iframe'], 19 | 20 | // Image/Audio/Video/Iframe hosts that should be retained (by default, all hosts are allowed). 21 | 'allowed_media_hosts' => null, 22 | ]; 23 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Cleaner/Cleaner.php: -------------------------------------------------------------------------------- 1 | config = $config; 19 | 20 | return $this; 21 | } 22 | 23 | public function config(): CleanerConfig 24 | { 25 | return $this->config; 26 | } 27 | 28 | public function clean(string $value): string 29 | { 30 | $value = $this->escapeElements($value); 31 | $value = $this->cleanMediaElements($value); 32 | 33 | return $this->config->shouldEscapeInlineListeners() 34 | ? $this->escapeInlineEventListeners($value) 35 | : $this->removeInlineEventListeners($value); 36 | } 37 | 38 | public function escapeElements(string $value): string 39 | { 40 | preg_match_all($this->config->elementsPattern(), $value, $matches); 41 | 42 | foreach (Arr::get($matches, '0', []) as $htmlElement) { 43 | $value = str_replace($htmlElement, e($htmlElement), $value); 44 | } 45 | 46 | return $value; 47 | } 48 | 49 | public function cleanMediaElements(string $value): string 50 | { 51 | if (!$this->config->allowedMediaHosts()) { 52 | return $value; 53 | } 54 | 55 | $allowedUrls = collect($this->config->allowedMediaHosts()) 56 | ->map( 57 | fn(string $host) => !Str::startsWith($host, ['http', 'https', '//']) 58 | ? ["http://{$host}", "https://{$host}", "//{$host}"] 59 | : [$host], 60 | ) 61 | ->flatten() 62 | ->all(); 63 | 64 | preg_match_all($this->config->mediaElementsPattern(), $value, $matches); 65 | 66 | foreach (Arr::get($matches, '0', []) as $htmlElement) { 67 | preg_match_all('/src="(.*)"/isU', $htmlElement, $sources); 68 | 69 | $urls = Arr::get($sources, '1', []); 70 | 71 | foreach ($urls as $url) { 72 | if (!Str::startsWith($url, $allowedUrls)) { 73 | $value = str_replace($url, '#!', $value); 74 | } 75 | } 76 | } 77 | 78 | return $value; 79 | } 80 | 81 | public function removeInlineEventListeners(string $value): string 82 | { 83 | foreach ($this->config->inlineListenersPatterns() as $pattern) { 84 | $value = preg_replace($pattern, '', $value); 85 | } 86 | 87 | return !is_string($value) ? '' : $value; 88 | } 89 | 90 | public function escapeInlineEventListeners(string $value): string 91 | { 92 | foreach ($this->config->inlineListenersPatterns() as $pattern) { 93 | $value = preg_replace_callback($pattern, [$this, 'escapeEqualSign'], $value); 94 | } 95 | 96 | return !is_string($value) ? '' : $value; 97 | } 98 | 99 | protected function escapeEqualSign(array $matches): string 100 | { 101 | return str_replace('=', '=', $matches[0]); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Cleaner/CleanerConfig.php: -------------------------------------------------------------------------------- 1 | |null 24 | */ 25 | protected ?array $allowedMediaHosts = null; 26 | 27 | protected string $inlineListenersPattern = '/\bon\w+\s*=\s*([\'"])(.*?)\1|javascript:[^"\' >]*/is'; 28 | 29 | protected string $malformedListenersPattern = '/\bon\w+\s*=\s*([\'"])?([^\'"\s>]+)\1?(?=\s|>)/i'; 30 | 31 | public static function make(): CleanerConfig 32 | { 33 | return new static(); 34 | } 35 | 36 | public static function fromArray(array $params): CleanerConfig 37 | { 38 | $config = static::make(); 39 | 40 | foreach ($params as $key => $value) { 41 | $setter = 'set' . Str::camel($key); 42 | 43 | if (method_exists($config, $setter)) { 44 | $config->{$setter}($value); 45 | } 46 | } 47 | 48 | return $config; 49 | } 50 | 51 | /** 52 | * Configures the given element as allowed. 53 | * 54 | * Allowed elements are elements the cleaner should retain from the input. 55 | */ 56 | public function allowElement(string $element): CleanerConfig 57 | { 58 | $this->allowedElements[] = $element; 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * Configures the given element as not allowed. 65 | * 66 | * Denied elements are elements the cleaner should escape from the input. 67 | */ 68 | public function denyElement(string $element): CleanerConfig 69 | { 70 | $this->deniedElements[] = $element; 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * Alias for ::denyElement() 77 | */ 78 | public function blockElement(string $element): CleanerConfig 79 | { 80 | return $this->denyElement($element); 81 | } 82 | 83 | /** 84 | * Configures the given element as media. 85 | * 86 | * Allowed elements are elements the cleaner should retain from the input. 87 | */ 88 | public function addMediaElement(string $element): CleanerConfig 89 | { 90 | $this->mediaElements[] = $element; 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * Configures the given element as not media. 97 | * 98 | * Allowed elements are elements the cleaner should retain from the input. 99 | */ 100 | public function removeMediaElement(string $element): CleanerConfig 101 | { 102 | $this->mediaElements = array_filter( 103 | $this->mediaElements, 104 | fn(string $el) => $el !== $element, 105 | ); 106 | 107 | return $this; 108 | } 109 | 110 | /** 111 | * Allows only a given list of hosts to be used in media source attributes (img, audio, video, iframe...). 112 | * 113 | * All other hosts will be dropped. By default, all hosts are allowed 114 | * ($allowMediaHosts = null). 115 | * 116 | * @param list|null $allowMediaHosts 117 | */ 118 | public function allowMediaHosts(?array $allowMediaHosts): CleanerConfig 119 | { 120 | $this->allowedMediaHosts = $allowMediaHosts; 121 | 122 | return $this; 123 | } 124 | 125 | public function elementsPattern(): string 126 | { 127 | $pattern = collect($this->deniedElements) 128 | ->reject(fn(string $element) => $this->allowedElements && in_array($element, $this->allowedElements)) 129 | ->map(fn(string $element) => "<{$element}.*{$element}>") 130 | ->implode('|'); 131 | 132 | return "/({$pattern})/isU"; 133 | } 134 | 135 | public function mediaElementsPattern(): string 136 | { 137 | $pattern = collect($this->mediaElements) 138 | ->map(fn(string $element) => "<{$element}.*{$element}>") 139 | ->implode('|'); 140 | 141 | return "/({$pattern})/isU"; 142 | } 143 | 144 | /** 145 | * @return list|array|string[] 146 | */ 147 | public function inlineListenersPatterns(): array 148 | { 149 | return [$this->inlineListenersPattern, $this->malformedListenersPattern]; 150 | } 151 | 152 | public function shouldEscapeInlineListeners(): bool 153 | { 154 | return $this->escapeInlineListeners; 155 | } 156 | 157 | public function allowedMediaHosts(): ?array 158 | { 159 | return $this->allowedMediaHosts; 160 | } 161 | 162 | public function setAllowedElements(?array $allowedElements): CleanerConfig 163 | { 164 | $this->allowedElements = $allowedElements; 165 | 166 | return $this; 167 | } 168 | 169 | public function setDeniedElements(array $deniedElements): CleanerConfig 170 | { 171 | $this->deniedElements = $deniedElements; 172 | 173 | return $this; 174 | } 175 | 176 | public function setBlockedElements(array $blockedElements): CleanerConfig 177 | { 178 | return $this->setDeniedElements($blockedElements); 179 | } 180 | 181 | public function setMediaElements(array $mediaElements): CleanerConfig 182 | { 183 | $this->mediaElements = $mediaElements; 184 | 185 | return $this; 186 | } 187 | 188 | public function setEscapeInlineListeners(bool $escapeInlineListeners): CleanerConfig 189 | { 190 | $this->escapeInlineListeners = $escapeInlineListeners; 191 | 192 | return $this; 193 | } 194 | 195 | public function setAllowedMediaHosts(?array $allowedMediaHosts): CleanerConfig 196 | { 197 | $this->allowedMediaHosts = $allowedMediaHosts; 198 | 199 | return $this; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/Facade/XSSCleaner.php: -------------------------------------------------------------------------------- 1 | except = config('xss-filter.except', []); 23 | } 24 | 25 | /** 26 | * Transform the given value. 27 | * 28 | * @param string $key 29 | * @param mixed $value 30 | * 31 | * @return string|mixed 32 | */ 33 | protected function transform($key, $value): mixed 34 | { 35 | if (in_array($key, $this->except, true)) { 36 | return $value; 37 | } 38 | 39 | if (!is_string($value)) { 40 | return $value; 41 | } 42 | 43 | return $this->cleaner->clean($value); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Middleware/FilterXSSLivewire.php: -------------------------------------------------------------------------------- 1 | isLivewireRequest($request)) { 15 | return $next($request); 16 | } 17 | 18 | $this->cleanLivewirePayload($request); 19 | 20 | return $next($request); 21 | } 22 | 23 | protected function cleanLivewirePayload(Request $request): void 24 | { 25 | $components = $request->input('components'); 26 | 27 | foreach ($components as $i => &$component) { 28 | if (isset($component['updates'])) { 29 | $component['updates'] = $this->cleanArray($component['updates'], "components.{$i}.updates."); 30 | } 31 | 32 | if (isset($component['calls'])) { 33 | foreach ($component['calls'] as $j => &$call) { 34 | $call['params'] = $this->cleanArray($call['params'], "components.{$i}.calls.{$j}.params."); 35 | } 36 | } 37 | } 38 | 39 | $request->request->set('components', $components); 40 | } 41 | 42 | protected function isLivewireRequest(Request $request): bool 43 | { 44 | return $request->routeIs('*livewire.update'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/XSSFilterServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 16 | __DIR__ . '/../config/xss-filter.php' => config_path('xss-filter.php'), 17 | ], 'config'); 18 | } 19 | 20 | public function register(): void 21 | { 22 | $this->mergeConfigFrom(__DIR__ . '/../config/xss-filter.php', 'xss-filter'); 23 | 24 | $this->app->scoped(Cleaner::class, static function () { 25 | return new Cleaner(CleanerConfig::fromArray(config('xss-filter'))); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/FilterXSSTest.php: -------------------------------------------------------------------------------- 1 | request = Request::create($url, 'POST', $data); 33 | 34 | return $this->request; 35 | } 36 | 37 | protected function responseFromMiddlewareWithInput(array $input = []): void 38 | { 39 | app(FilterXSS::class) 40 | ->handle($this->request($input), function () { 41 | // nothing to do here 42 | }); 43 | } 44 | 45 | #[Test] 46 | public function it_doesnt_change_non_html_inputs(): void 47 | { 48 | $this->responseFromMiddlewareWithInput($input = ['text' => 'Simple text', 'number' => 56]); 49 | 50 | $this->assertEquals($input, $this->request->all()); 51 | } 52 | 53 | #[Test] 54 | public function it_escapes_script_tags(): void 55 | { 56 | $this->responseFromMiddlewareWithInput([ 57 | 'with_src' => 'Before text after text', 58 | 'multiline' => "Before text \n \n After text", 59 | ]); 60 | 61 | $this->assertEquals([ 62 | 'with_src' => 'Before text ' . e('') . ' after text', 63 | 'multiline' => "Before text \n " . e("") . "\n After text", 64 | ], $this->request->all()); 65 | } 66 | 67 | #[Test] 68 | public function it_doesnt_change_non_script_html_inputs(): void 69 | { 70 | $this->responseFromMiddlewareWithInput([ 71 | 'html_with_script_src' => '
link textBefore text after text
test on some text test test test', 72 | 'html_with_script_multiline' => "
\nlink text\n Before text \n \n After text
\n test on some text test test test", 73 | ]); 74 | 75 | $this->assertEquals([ 76 | 'html_with_script_src' => '
link textBefore text ' . e('') . ' after text
test on some text test test test', 77 | 'html_with_script_multiline' => "
\nlink text\n Before text \n " . e("") . "\n After text
\n test on some text test test test", 78 | ], $this->request->all()); 79 | } 80 | 81 | #[Test] 82 | public function it_escapes_embed_elements(): void 83 | { 84 | $this->responseFromMiddlewareWithInput([ 85 | 'iframe' => '
Before text after text.
', 86 | 'iframe_multiline' => '
\nBefore text\n\n after text.\n
', 87 | 'object' => '
Before text after text.
', 88 | 'object_multiline' => '
\nBefore text\n\n after text.\n
', 89 | ]); 90 | 91 | $this->assertEquals([ 92 | 'iframe' => '
Before text' . e('') . ' after text.
', 93 | 'iframe_multiline' => '
\nBefore text\n' . e('') . '\n after text.\n
', 94 | 'object' => '
Before text' . e('') . ' after text.
', 95 | 'object_multiline' => '
\nBefore text\n' . e('') . '\n after text.\n
', 96 | ], $this->request->all()); 97 | } 98 | 99 | #[Test] 100 | public function it_removes_inline_listeners(): void 101 | { 102 | $this->responseFromMiddlewareWithInput([ 103 | 'html' => '

Text ...

', 104 | 'html_multiline' => "
\n

\nText ...

\n
", 105 | ]); 106 | 107 | $this->assertEquals([ 108 | 'html' => '

Text ...

', 109 | 'html_multiline' => "
\n

\nText ...

\n
", 110 | ], $this->request->all()); 111 | } 112 | 113 | #[Test] 114 | public function it_removes_img_inline_listeners(): void 115 | { 116 | $this->responseFromMiddlewareWithInput([ 117 | 'html' => 'test', 118 | ]); 119 | 120 | $this->assertEquals([ 121 | 'html' => 'test', 122 | ], $this->request->all()); 123 | } 124 | 125 | #[Test] 126 | public function it_removes_inline_listeners_with_string_params(): void 127 | { 128 | $this->responseFromMiddlewareWithInput([ 129 | 'html' => 'test', 130 | 'html' => 'test', 131 | ]); 132 | 133 | $this->assertEquals([ 134 | 'html' => 'test', 135 | 'html' => 'test', 136 | ], $this->request->all()); 137 | } 138 | 139 | #[Test] 140 | public function it_removes_inline_listeners_from_invalid_html(): void 141 | { 142 | $this->responseFromMiddlewareWithInput([ 143 | 'html' => '

Text ...

', 144 | 'html_multiline' => "
\n

\nText ...

\n
", 145 | ]); 146 | 147 | $this->assertEquals([ 148 | 'html' => '

Text ...

', 149 | 'html_multiline' => "
\n

\nText ...

\n
", 150 | ], $this->request->all()); 151 | } 152 | 153 | #[Test] 154 | public function it_clears_nested_inputs(): void 155 | { 156 | $this->responseFromMiddlewareWithInput([ 157 | 'value1' => 'Value 1', 158 | 'value2' => 2, 159 | 'html' => [ 160 | 'oneline' => '

Text ...

link textBefore text after text
', 161 | 'multline' => "
\n

\nText ...

\n
\n
\nlink text\n Before text \n \n After text
", 162 | ], 163 | 'value3' => [ 164 | 'value3_1' => 'Value 3-1', 165 | 'value3_2' => 32, 166 | ], 167 | ]); 168 | 169 | $this->assertEquals([ 170 | 'value1' => 'Value 1', 171 | 'value2' => 2, 172 | 'html' => [ 173 | 'oneline' => '

Text ...

link textBefore text ' . e('') . ' after text
', 174 | 'multline' => "
\n

\nText ...

\n
\n
\nlink text\n Before text \n " . e("") . "\n After text
", 175 | ], 176 | 'value3' => [ 177 | 'value3_1' => 'Value 3-1', 178 | 'value3_2' => 32, 179 | ], 180 | ], $this->request->all()); 181 | } 182 | 183 | #[Test] 184 | public function it_dont_convert_0_to_empty_string(): void 185 | { 186 | $this->responseFromMiddlewareWithInput($input = ['text' => '0']); 187 | 188 | $this->assertEquals($input, $this->request->all()); 189 | } 190 | 191 | #[Test] 192 | public function it_removes_inline_javascript_in_href(): void 193 | { 194 | $this->responseFromMiddlewareWithInput([ 195 | 'html' => '', 196 | 'html_multiline' => "
\n

\nLink\n

\n
", 197 | ]); 198 | 199 | $this->assertEquals([ 200 | 'html' => '', 201 | 'html_multiline' => "
\n

\nLink\n

\n
", 202 | ], $this->request->all()); 203 | } 204 | 205 | #[Test] 206 | public function it_doest_not_touch_other_attributes(): void 207 | { 208 | $this->responseFromMiddlewareWithInput([ 209 | 'html' => '

text

', 210 | 'html_multiline' => "

\n\ntext\n\n

", 211 | ]); 212 | 213 | $this->assertEquals([ 214 | 'html' => '

text

', 215 | 'html_multiline' => "

\n\ntext\n\n

", 216 | ], $this->request->all()); 217 | } 218 | 219 | #[Test] 220 | public function it_escapes_inline_event_listeners(): void 221 | { 222 | XSSCleaner::config()->setEscapeInlineListeners(true); 223 | 224 | $this->responseFromMiddlewareWithInput([ 225 | 'html' => '

text

', 226 | 'html_multiline' => "

\n\ntext\n\n

", 227 | ]); 228 | 229 | $this->assertEquals([ 230 | 'html' => '

text

', 231 | 'html_multiline' => "

\n\ntext\n\n

", 232 | ], $this->request->all()); 233 | } 234 | 235 | #[Test] 236 | public function it_cleans_disallowed_media_hosts(): void 237 | { 238 | XSSCleaner::config()->allowElement('iframe')->allowMediaHosts(['youtube.com']); 239 | 240 | $this->responseFromMiddlewareWithInput([ 241 | 'iframe' => '
Before text after text.
', 242 | 'iframe_multiline' => '
\nBefore text\n\n after text.\n
', 243 | 'video' => '
Before text after text.
', 244 | 'video_multiline' => '
\nBefore text\n\n after text.\n
', 245 | ]); 246 | 247 | $this->assertEquals([ 248 | 'iframe' => '
Before text after text.
', 249 | 'iframe_multiline' => '
\nBefore text\n\n after text.\n
', 250 | 'video' => '
Before text after text.
', 251 | 'video_multiline' => '
\nBefore text\n\n after text.\n
', 252 | ], $this->request->all()); 253 | } 254 | 255 | #[Test] 256 | public function it_does_not_escape_allowed_media_hosts(): void 257 | { 258 | XSSCleaner::config()->allowElement('iframe')->allowMediaHosts([ 259 | 'example.test', 260 | 'https://video.test', 261 | 'youtu.be', 262 | ]); 263 | 264 | $this->responseFromMiddlewareWithInput([ 265 | 'iframe' => '
Before text after text.
', 266 | 'iframe_multiline' => '
\nBefore text\n\n after text.\n
', 267 | 'video' => '
Before text after text.
', 268 | 'video_multiline' => '
\nBefore text\n\n after text.\n
', 269 | ]); 270 | 271 | $this->assertEquals([ 272 | 'iframe' => '
Before text after text.
', 273 | 'iframe_multiline' => '
\nBefore text\n\n after text.\n
', 274 | 'video' => '
Before text after text.
', 275 | 'video_multiline' => '
\nBefore text\n\n after text.\n
', 276 | ], $this->request->all()); 277 | } 278 | } 279 | --------------------------------------------------------------------------------