Security Tip: Avoiding XSS with HtmlString

[Tip#44] Check out that one simple trick... I mean... This is my favourite way to avoid XSS.

Security Tip: Avoiding XSS with HtmlString

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.

💡
Note, I'll specifically talk about the Blade tags, but these concepts apply across any escaped/unescaped output handling.

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.

💡
Stored XSS → the injected code is stored in the application and returned any time a user visits the vulnerable page.
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


🕵️
Worried about your app being hacked? Book in a Laravel Security Audit and Penetration Test! I can find the vulnerabilities before a hacker does, and help you fix them!
🥷
Learn to Think Like a Hacker with my hands-on practical security course: Practical Laravel Security!