Security 12 min read

Laravel XSS Protection: Blade, Livewire, and Raw Output

Cross-site scripting bypasses Laravel's default escaping more often than you think. Cover Blade's triple-brace pitfall, Livewire injection, and raw HTML output.

Matt King
Matt King
April 27, 2026
Last updated: April 27, 2026
Laravel XSS Protection: Blade, Livewire, and Raw Output

Cross-site scripting (XSS) is consistently in the OWASP Top 10 and is the most common vulnerability class in web applications. Laravel provides strong defaults against XSS, but those defaults have specific gaps that developers need to understand.

This guide covers how Laravel's XSS protections work, where they break down, and how to handle every context where user data appears in your HTML.


How Laravel Prevents XSS by Default

Blade auto-escaping

When you use double curly braces in Blade templates, Laravel automatically calls htmlspecialchars() on the output:

{{-- SAFE: auto-escaped --}}
<p>Hello, {{ $user->name }}</p>

{{-- If $user->name is "<script>alert(1)</script>", outputs: --}}
<p>Hello, &lt;script&gt;alert(1)&lt;/script&gt;</p>

This converts <, >, ", ', and & into HTML entities, preventing the browser from interpreting them as HTML or JavaScript.

Where auto-escaping is NOT enough

Blade escaping only works in the HTML body context. It does not protect you in these situations:

  1. Unescaped output ({!! !!})
  2. JavaScript contexts (inline scripts, event handlers)
  3. HTML attributes (especially href, src, style)
  4. URL contexts (user-controlled redirects)

Each of these requires a different defense.


The {!! !!} Problem

The most common XSS source in Laravel applications is {!! !!}, which outputs raw HTML without escaping.

When developers use {!! !!}

{{-- Rendering markdown-to-HTML --}}
{!! $post->rendered_content !!}

{{-- CMS WYSIWYG content --}}
{!! $page->body !!}

{{-- SVG icons --}}
{!! $icon !!}

When this becomes dangerous

{{-- VULNERABLE: user bio stored as HTML --}}
{!! $user->bio !!}

{{-- VULNERABLE: comment with "allowed HTML" --}}
{!! $comment->body !!}

If a user stores <img src=x onerror=alert(document.cookie)> as their bio, every visitor to their profile has their session cookie stolen.

How to safely render user HTML

Use HTMLPurifier to sanitize HTML before rendering:

// Install: composer require mews/purifier

// In your model or service:
use Mews\Purifier\Facades\Purifier;

public function getCleanBioAttribute(): string
{
    return Purifier::clean($this->bio);
}
{{-- Safe: sanitized before rendering --}}
{!! $user->clean_bio !!}

HTMLPurifier removes dangerous tags (<script>, <iframe>, event handlers) while preserving safe formatting (<p>, <strong>, <a> with safe hrefs).

Audit your templates

Find every {!! !!} in your codebase:

grep -rn '{!!' resources/views/

For each result, answer: "Can a user control this content?" If yes, sanitize it or switch to {{ }}.


JavaScript Context XSS

Blade escaping does not help when you inject data into JavaScript:

{{-- VULNERABLE: Blade escaping does not help here --}}
<script>
    let username = "{{ $user->name }}";
</script>

If the username contains "; alert(document.cookie); ", the script becomes:

let username = ""; alert(document.cookie); "";

Safe patterns for JavaScript data

Option 1: Use @json (recommended)

<script>
    let username = @json($user->name);
    let settings = @json($settings);
</script>

@json uses json_encode() with JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT flags, which escapes all dangerous characters for JavaScript contexts.

Option 2: Data attributes

<div id="app" data-user="{{ $user->name }}">
const username = document.getElementById('app').dataset.user;

Blade's HTML escaping works correctly in data attributes because the browser decodes HTML entities when reading attribute values.


HTML Attribute XSS

Some HTML attributes are inherently dangerous:

{{-- VULNERABLE: user-controlled href --}}
<a href="{{ $user->website }}">Visit website</a>

If $user->website is javascript:alert(document.cookie), the user's session is stolen on click. Blade escaping does not prevent this because javascript: is a valid URL scheme.

Safe pattern

Validate URLs before rendering:

// In your model or form request
public function getValidWebsiteAttribute(): ?string
{
    $url = $this->website;

    if (!$url) return null;

    // Only allow http and https schemes
    if (!preg_match('/^https?:\/\//i', $url)) {
        return null;
    }

    return $url;
}
@if($user->valid_website)
    <a href="{{ $user->valid_website }}">Visit website</a>
@endif

Style attribute injection

{{-- VULNERABLE --}}
<div style="background-color: {{ $user->color }}">

If $user->color is red; background-image: url(javascript:alert(1)), it can execute scripts in older browsers. Validate against an allowlist of values instead.


Content Security Policy

CSP is the nuclear option against XSS. Even if an attacker successfully injects a script, CSP prevents the browser from executing it.

Basic CSP for Laravel

Add this middleware:

namespace App\Http\Middleware;

class ContentSecurityPolicy
{
    public function handle($request, $next)
    {
        $response = $next($request);

        $response->headers->set(
            'Content-Security-Policy',
            "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'"
        );

        return $response;
    }
}

Register it in your HTTP kernel or route middleware.

Start with report-only

If adding CSP to an existing application, start in report-only mode to avoid breaking functionality:

$response->headers->set(
    'Content-Security-Policy-Report-Only',
    "default-src 'self'; script-src 'self'; report-uri /csp-report"
);

This logs violations without blocking them. Review the reports, adjust your policy, then switch to enforcement.

Handling inline scripts with nonces

If you use inline scripts (Alpine.js, Livewire), use nonces:

// Generate a nonce per request
$nonce = base64_encode(random_bytes(16));

$response->headers->set(
    'Content-Security-Policy',
    "script-src 'self' 'nonce-{$nonce}'"
);
<script nonce="{{ $nonce }}">
    // This will execute because the nonce matches
</script>

Livewire and Alpine.js Considerations

If you use Livewire, be aware of these XSS patterns:

{{-- VULNERABLE: wire:model with {!! !!} --}}
<div>{!! $this->userContent !!}</div>

{{-- SAFE: Livewire auto-escapes wire:model output --}}
<div>{{ $this->userContent }}</div>

Alpine.js x-html is equivalent to {!! !!} and does not escape:

{{-- VULNERABLE: Alpine renders raw HTML --}}
<div x-html="userBio"></div>

{{-- SAFE: Alpine text binding escapes --}}
<div x-text="userBio"></div>

XSS Prevention Checklist

  1. Use {{ }} for all output by default
  2. Audit every {!! !!} and sanitize with HTMLPurifier if user-controlled
  3. Use @json() when passing data to JavaScript
  4. Validate URL schemes before rendering in href and src attributes
  5. Implement Content Security Policy headers
  6. Use x-text instead of x-html in Alpine.js
  7. Run StackShield to detect XSS-prone patterns externally

Continuous Monitoring

XSS vulnerabilities are introduced over time as new features are built. A template that was safe last month may have a new {!! !!} added by a developer who did not know the data source was user-controlled.

StackShield monitors your application externally and flags XSS-prone responses, missing CSP headers, and other security regressions. Combined with code review practices and CSP enforcement, you can keep XSS out of your Laravel application permanently.

XSS is category A03 (Injection) in the OWASP Top 10. For a full breakdown of all ten categories and their Laravel-specific fixes, see our OWASP Top 10 in Laravel guide.

Check your application's XSS posture with a free scan.

Free security check

Is your Laravel app exposed right now?

34% of Laravel apps we scan have at least one critical issue. Most teams don't find out until something breaks. Our free scan checks your live application in under 60 seconds.

18% have debug mode on
72% missing security headers
12% have exposed .env
Scan My App Free No signup required. Results in 60 seconds.

Frequently Asked Questions

Does Laravel prevent XSS automatically?

Partially. Laravel's Blade templating engine automatically escapes output when you use {{ $variable }} syntax, converting characters like <, >, and " into HTML entities. This prevents most reflected and stored XSS in HTML body contexts. However, XSS can still occur when using {!! !!} for unescaped output, when injecting data into JavaScript contexts, HTML attributes, or URLs, and when rendering user-generated HTML content.

When is it safe to use {!! !!} in Blade?

Only when the content is generated by your application (not user input) or has been sanitized with a library like HTMLPurifier. Common safe uses include rendering markdown that you converted to HTML server-side, displaying admin-authored CMS content, and outputting JSON-LD structured data. Never use {!! !!} with raw user input, URL parameters, form submissions, or database fields that users can edit.

What is Content Security Policy and do I need it for XSS protection?

Content Security Policy (CSP) is an HTTP header that tells browsers which sources of scripts, styles, and other resources are allowed on your page. Even if an attacker injects a script tag via XSS, a strict CSP prevents it from executing. CSP is the strongest defense against XSS available and is recommended for all production applications. Start with report-only mode to understand your resource loading patterns before enforcing.

How do I test for XSS in my Laravel application?

Test every input field with common XSS payloads: <script>alert(1)</script>, " onmouseover="alert(1), and javascript:alert(1). Check both reflected XSS (does the payload appear in the response?) and stored XSS (does the payload execute when viewing saved data?). Automated tools like StackShield can detect XSS-prone patterns in your application's external responses.

Stay Updated on Laravel Security

Get actionable security tips, vulnerability alerts, and best practices for Laravel apps.

, \" onmouseover=\"alert(1), and javascript:alert(1). Check both reflected XSS (does the payload appear in the response?) and stored XSS (does the payload execute when viewing saved data?). Automated tools like StackShield can detect XSS-prone patterns in your application's external responses." } } ] }