Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Xss Protection Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. config/xss-protection.php — the patterns array is the whole policy; read it before trusting it.
  2. The middleware class itself — short, and shows exactly which parts of the request are touched (query, request, cookies, sometimes content).
  3. The repo's test suite — it's the most accurate documentation of what is and isn't caught.

Implementation Patterns

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.

Gotchas and Tips

  • Blacklist by regex is inherently incomplete. Obfuscated payloads (HTML entities, \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.
  • It mutates input, which breaks identity and signatures. Webhook HMAC verification over the raw body, idempotency keys derived from payloads, and content hashes will fail once the middleware rewrites the request. Exclude those routes, or ensure the middleware runs after signature verification.
  • *** 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".
  • Watch middleware ordering. It should generally run early (so nothing downstream sees raw input) but after anything that reads the raw body — CORS preflight, signature verification, TrimStrings/ConvertEmptyStringsToNull behavior. Bump it in Http/Kernel.php if order matters.
  • Not every part of the request is covered. File uploads, headers, and raw non-form content types behave differently. Verify with a real multipart or application/json request rather than assuming.
  • Config caching. After editing patterns in production, php artisan config:cache — otherwise your new patterns silently don't apply and you'll chase a phantom.
  • Debugging "is it even running?" Use 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.
  • Over-matching is the real risk, not under-matching. Patterns like 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.
  • Small package, verify fit. With ~150 stars and a single maintainer, pin the version, run your own smoke tests against your Laravel version after upgrades, and be prepared to fork or inline the middleware — it's a thin class, so replacing it with a project-local copy is a low-cost exit. 1.11.0 is the release that brought Laravel 13.x support; if you're on 13, that's your minimum, and if you're still on 10/11/12 there's nothing urgent in it.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings