protonemedia/laravel-xss-protection
Laravel XSS Protection provides middleware and validation rules that sanitize user input to guard against cross-site scripting. It recursively cleans request data, supports custom filters, and integrates smoothly with Laravel forms and Blade templates.
Install and publish the config:
composer require protonemedia/laravel-xss-protection
php artisan vendor:publish --tag=xss-protection-config
The service provider is auto-discovered. On boot it registers a middleware (ProtoneMedia\LaravelXssProtection\Middleware\XssProtection) that walks the incoming request — query string, request body, and cookies — applies a list of regex patterns, and replaces each match with a replacement string (default is typically ***). You choose in config/xss-protection.php whether it runs globally or is appended to the web group.
Version note: 1.11.0 adds Laravel 13.x compatibility (plus a docs link for Inertia Modal users). No config or API changes — it's a compatibility release, so the setup below is unchanged from 1.10.x. Pin ^1.11 if you're on Laravel 13.
First use case: any app that echoes user input back — profile fields, comments, an admin panel showing submitted form data, or a legacy Blade view using {!! !!}. Add the middleware, then poke a route:
GET /search?q=<script>alert(1)</script>
and confirm inside the controller that $request->input('q') comes back sanitized. That single round-trip tells you the middleware is in the stack and the patterns are effective.
Where to look first:
config/xss-protection.php — the patterns array is the whole policy; read it before trusting it.query, request, cookies, sometimes content).Register it for the web group, not globally, by default. Most apps have at least one endpoint that must receive raw payloads (webhooks, a markdown/rich-text editor, a signature-verified integration). Scoping to web keeps api and integration routes predictable, and you opt specific routes in instead.
Opt-in per route group for API surfaces. If your API returns JSON that a SPA renders through its own escaping, you usually don't want input mutation there. Register the middleware alias and apply it deliberately:
Route::middleware('xss')->group(function () {
Route::post('/comments', [CommentController::class, 'store']);
});
Sanitize before validation, and let FormRequests inherit it. Because it's middleware, $request->all() is already cleaned by the time your FormRequest rules run. This is usually what you want (a <script> payload fails string|max:255 on the cleaned value, not the raw one), but see the gotchas about *** inflating length.
Customize patterns for your domain. Add the vectors you actually care about — event handlers, data: URIs, expression(, protocol-relative javascript: variants — and keep delimiters/i flags consistent:
'patterns' => [
'/<\s*script/i',
'/<\s*\/\s*script\s*>/i',
'/\bon\w+\s*=/i',
'/javascript\s*:/i',
'/vbscript\s*:/i',
'/data\s*:\s*text\/html/i',
],
Exclude endpoints that legitimately carry markup. Rich-text editors (Trix, Quill, TinyMCE) POST HTML that this middleware will mangle. Either exclude those routes from the middleware and run a whitelist sanitizer (e.g. HTMLPurifier / mews/purifier) on the specific fields, or sanitize only the known-risky fields in the controller. The same applies to Inertia apps: Inertia props round-trip through JSON, so form submissions carrying markup hit the same problem — handle those with a field-level sanitizer rather than the global middleware.
Use it as a detection signal, not just a filter. If you wrap or extend the middleware, you can log when $request->all() !== $cleaned (or when the replacement string appears) and ship those to your security channel. That turns a blunt filter into a useful tripwire for scanning traffic.
Test the two directions. One feature test posts a known payload and asserts the stored value contains no <script; another posts benign look-alike content (5 < 10, a code snippet containing onerror=, or the literal text javascript:) and asserts it survives — that second test is what stops you from silently corrupting user content.
Programmatic reuse. For queue jobs or console commands that bypass HTTP, don't assume the middleware applies. Either extract the pattern list into your own small sanitizer service or call the package's sanitizer class directly if exposed — otherwise non-HTTP entry points are unprotected.
\u003c, nested/whitespace-padded tags, unusual encodings) will get through. This package is a safety net layered on top of Blade escaping and a CSP header — not a replacement for either.*** changes length. string|max:255 rules can start rejecting inputs that were previously valid, and stored values now contain *** where the user typed something. If you'd rather neutralize than annotate, change replacement to '' — but then required rules can start failing on inputs that were effectively "only a script tag".TrimStrings/ConvertEmptyStringsToNull behavior. Bump it in Http/Kernel.php if order matters.application/json request rather than assuming.php artisan config:cache — otherwise your new patterns silently don't apply and you'll chase a phantom.php artisan route:list -v to confirm the middleware appears on the route. If not, the global/group registration in config isn't doing what you think. A temporary dd($request->all()) at the top of a test route settles it fast.on\w+= will chew up legitimate prose and code samples. Keep patterns anchored and specific; review the diff in your test fixtures whenever you add one.How can I help you explore Laravel packages today?