masterro/laravel-xss-filter
A Laravel package for filtering XSS from user input. It offers configurable sanitization and middleware to clean request data, helping protect your application from cross-site scripting attacks with minimal setup.
Install with Composer:
composer require masterro/laravel-xss-filter
Publish the config file (check the README for the exact provider or tag in your version):
php artisan vendor:publish --provider="MasterRO\LaravelXssFilter\Providers\LaravelXssFilterServiceProvider"
Open config/xss-filter.php first. This is where you define the HTML you want to keep:
allowed_tags: e.g. p, a, strong, em, ul, ol, li, blockquote, code, pre, br, img.allowed_attributes: usually per-tag, e.g. a => ['href', 'title'], img => ['src', 'alt', 'title', 'width', 'height'].Register the middleware. For a traditional server-rendered app, add it to the web group so all form input is filtered:
// app/Http/Kernel.php
protected $middlewareGroups = [
'web' => [
// ...
\MasterRO\LaravelXssFilter\Middleware\XssFilter::class,
],
];
Or register an alias and apply it only where needed:
protected $routeMiddleware = [
'xss' => \MasterRO\LaravelXssFilter\Middleware\XssFilter::class,
];
Route::post('/posts', [PostController::class, 'store'])->middleware('xss');
First use case: a blog post or comment form with a WYSIWYG editor. Submit a payload like:
<script>alert(1)</script><p>Hello <strong>world</strong></p>
After the middleware runs, $request->input('body') should be:
<p>Hello <strong>world</strong></p>
Test the filter directly in Tinker or a unit test if the package exposes its filter service/facade, e.g. XssFilter::clean($payload). Check the package README for the exact class or facade name in your version.
Global middleware on the web group: simplest for classic Laravel form apps. Every request input is sanitized before it reaches your controller, so you can treat $request->input() as “XSS-filtered but HTML-preserving” for fields that match your allowlist.
Route-specific middleware: better when only some endpoints accept HTML. Apply xss to post/comment/description routes, and leave login, password reset, search, and API endpoints untouched. This avoids mangling passwords or code snippets that legitimately contain <, >, or &.
FormRequest + targeted filtering: validate first, then filter only rich-text fields. If the package’s middleware is too broad, use its filter service in passedValidation() or the controller:
public function store(StorePostRequest $request)
{
$data = $request->validated();
$data['body'] = XssFilter::clean($data['body']);
Post::create($data);
}
This keeps passwords, emails, and structured JSON untouched.
Admin vs. public allowlists: public comments usually need a strict set (p, a, strong, em, ul, li). Admin WYSIWYG content may need images, tables, headings, etc. Because the package config is global, either:
API/JSON endpoints: be careful. Middleware that calls $request->merge($cleaned) on all input may recursively filter arrays, but verify nested JSON behavior. For APIs that accept rich text, filter only the specific string fields. For APIs that accept binary/base64/signed payloads, do not apply the middleware at all.
Blade output: filtering input is defense-in-depth, not a replacement for output escaping. Continue using {{ }} for normal text and reserve {!! !!} for fields you have explicitly filtered and trust. If you both filter on input and escape on output, users may see HTML entities like <p> — decide where filtering happens.
Testing pattern: add a feature test that posts a known payload and asserts:
<script> is gone,onerror, onclick, and javascript: URLs are removed.$response = $this->post('/posts', [
'body' => '<script>alert(1)</script><p>Hello <strong>world</strong></p>',
]);
$this->assertDatabaseMissing('posts', ['body' => '<script>alert(1)</script>']);
$this->assertStringContainsString('<strong>world</strong>', Post::first()->body);
Layered security: pair the package with validation, output escaping, CSP headers, and CSRF protection. Input filtering alone does not protect against stored XSS if templates render raw HTML carelessly.
It can alter legitimate non-HTML input. Passwords, API keys, mathematical expressions, and code snippets may contain < or &. Do not apply the middleware globally if your app has such fields — use route-specific middleware or filter only rich-text fields.
HTMLPurifier can be slow. If the package uses HTMLPurifier under the hood, filtering every request has a cost. Apply it only to endpoints that accept HTML, and cache configuration in production.
Config cache quirks. After editing config/xss-filter.php, run:
php artisan config:clear
In production, rebuild with php artisan config:cache. Otherwise your new allowed tags may not take effect.
Allowed attributes are per-tag. Do not allow style, on*, or arbitrary attributes. Keep the allowlist minimal. Test href="javascript:alert(1)" and img src="x" onerror="alert(1)" explicitly.
Protocol filtering matters. HTMLPurifier usually blocks javascript: URLs, but if you customize attributes or bypass the purifier, you can reintroduce the vulnerability. Verify mailto:, tel:, and relative URLs still work for your use case.
Nested arrays and JSON. Confirm whether the middleware recursively cleans nested input. If not, write a small recursive helper:
function cleanInput(array $input): array
{
return array_map(function ($value) {
return is_array($value) ? cleanInput($value) : XssFilter::clean($value);
}, $input);
}
File uploads are out of scope. The package filters input strings, not uploaded files. Validate MIME types, extensions, and storage paths separately.
Double-escaping confusion. If you filter on input and also escape on output with {{ }}, stored HTML may display as entities. If you render filtered HTML with {!! !!}, make sure the field is definitely filtered and that your allowlist is strict.
Debugging. Test payloads directly in Tinker or a small unit test. Compare $request->all() before and after the middleware. If the package exposes a clean() method, call it directly with representative payloads rather than debugging through a full HTTP request.
Extension point. The published config is the main extension point. For per-form rules, create a custom middleware or service that instantiates the filter with a different allowed-tag set, rather than editing the global config for one edge case.
Maintenance and pinning. This is a small package (41 stars). Pin the version in composer.json, review the changelog on upgrade, and keep your own XSS tests so you can detect behavior changes. The 2026 release date suggests it is maintained, but community support is limited compared with larger security packages.
Do not rely on it alone. Treat it as one layer. Validate input, escape output, use CSP, and review any place where you render user HTML with {!! !!}.
How can I help you explore Laravel packages today?