Security Tip: Avoiding XSS with HtmlString
[Tip#44] Check out that one simple trick... I mean... This is my favourite way to avoid XSS.
#1 → Exposed API Keys & Passwords
#2 → Missing Authorisation
#3 → Missing Content Security Policy (CSP)
#4 → Missing Security Headers
#5 → Insecure Function Use
#6 → Outdated & Vulnerable Dependencies
#7 → Cross-Site Scripting (XSS)
#8 → Insufficient Rate Limiting
#9 → Missing Subresource Integrity (SRI)
#10 → Insufficient Input Validation & Mass-Assignment Vulnerabilities
Cross-Site Scripting (XSS) usually occurs in Laravel apps when we use the unescaped blade tags ({!! ... !!}
), or the raw-html directives in Vue (v-html
) and Alpine (x-html
). These tags and directives output the value as-is, without removing any HTML or encoding special characters.
While there are many legitimate reasons to use these unescaped tags - such as using Markdown, WYSIWYG editors, or building complex HTML structures directly in code - they do pose a massive security risk, and possibly in a way you do not expect.
Consider the following code:
<div>
<h1>{{ $title }}</h1>
<p>{!! $description !!}</p>
{!! $images !!}
<ul>
@foreach ($items as $item)
<li>{!! $item->name !!} - {{ $item->price }}</li>
@endforeach
</ul>
{!! $notes !!}
{!! $buttons !!}
</div>
There are 5 unescaped blade tags in this block of code. The reasons for being unescaped can be easily guessed for the following three:
$images
→ Probably returns image HTML$notes
→ Most likely generated from Markdown or a WYSIWYG/HTML editor.$buttons
→ Buttons would be HTML, right?
We can also make the assumption for $description
and $item->name
including some markdown… right?
Wrong!
$item->name
comes directly from user input and loading it unescaped on the page opens up a massive Stored XSS vulnerability! It wasn't supposed to be outputted unescaped, but because the unescaped tags get used so frequently, the developers didn't notice and the vulnerability was introduced.
Reflected XSS → the injected code is passed into the request it’s loaded on, and requires users to visit a specific link or submit a form to trigger it.
This is the reason why unescaping tags pose a massive security risk. If you use them frequently, you lose visibility of when they shouldn’t be used.
The HtmlString Helper
The way to avoid this risk is to use Laravel’s Illuminate\Support\HtmlString
helper class any time you need to output generated HTML on the page. It implements the Illuminate\Contracts\Support\Htmlable
interface, which Laravel checks for in it’s escaping function e()
.
Simply wrap your HTML inside the class HtmlString
and the escaping function will return the raw HTML value.
So for our $notes
variable above, we can do this when we render it in markdown:
$notes = new HtmlString(Str::markdown($rawNotes, [
'html_input' => 'strip',
'allow_unsafe_links' => false,
]));
Or using the Fluent string interface:
$notes = Str::of($rawNotes)
->markdown([
'html_input' => 'strip',
'allow_unsafe_links' => false,
])
->toHtmlString();
Once you wrap all of your safe HTML variables inside HtmlString
, you can output them using the escaping tags:
<div>
<h1>{{ $title }}</h1>
<p>{{ $description }}</p>
{{ $images }}
<ul>
@foreach ($items as $item)
<li>{!! $item->name !!} - {{ $item->price }}</li>
@endforeach
</ul>
{{ $notes }}
{{ $buttons }}
</div>
Suddenly our XSS vector on $item->name
jumps out at us as the only use of {!! ... !!}
, and we immediately know it’s suspicious. It won’t take long for you to dig into it, figure out it’s vulnerable to XSS, and fix it. 😎
Cross-Site Scripting (XSS) Resources
- Security Tip: Validating HTML & Markdown Input
- In Depth: Escaping Output Safely
- Security Tip: Safely Rendering JSON in Blade
- In Depth: Content Security Policy
- Practical Laravel Security course