Security Tip: Avoiding XSS with HtmlString

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

Security Tip: Avoiding XSS with HtmlString

Greetings my friends! We’re onto #7 in my list of Top 10 security issues I’ve found during audits: Cross-Site Scripting (XSS)! This is a topic that needs no introduction to most folks, and today I want to tell you that one simple trick that can save your website from XSS attacks1 my favourite way to avoid XSS sneaking into your apps. We’ve covered it before, but not on it’s own, so I wanted to dedicate a tip to it specifically. I’ll link through to my other XSS related articles at the end for further learning. 🙂

Here’s our current Top 10:
#10 → Insufficient Input Validation
#9 → Missing Subresource Integrity (SRI)
#8 → Insufficient Rate Limiting
#7 → Cross-Site Scripting (XSS)

🛡️ I can help you find and fix vulnerabilities in your code, book a security audit now! 🕵️

Looking to learn more?
Security Tip #29: Protecting Production APIs
▶️ In Depth #11: Insecure Direct Object References (IDOR

Please consider becoming a paid subscriber to support Laravel Security in Depth.
You’ll receive weekly security tips and monthly In Depth articles, covering every aspect of building secure applications in Laravel.

Avoiding XSS with HtmlString

Cross-Site Scripting (XSS) usually occurs in Laravel apps when we use the unescaped blade tags (`{!! ... !!}`), or directives in Vue (`v-html`) and Alpine (`x-html`)2. 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 tags3, they do pose a massive security risk, and possibly in a way you do not expect.

Consider this 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 WYSIWYG4

  • `$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 vulnerability5! 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.

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 class6 any time you need to output generated HTML on the page. It implements the `Illuminate\Contracts\Support\Htmlable` interface7, 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 to dig into it, figure out it’s vulnerable, and fix it. 😎

Cross-Site Scripting (XSS) Resources


  1. 😉

  2. Note, we’ll be specifically talking about Blade tags and a way to avoid XSS with them, but some of the concepts can be applied across the JS frameworks too.

  3. Such as: using markup like Markdown to format text, displaying output from WYSIWYG editors, building complex HTML structures in code rather than in templates and components, constructing lists from collections with markup, etc…

  4. WYSIWYG stands for "What You See Is What You Get" and refers to an editor that allows users to create and edit content visually, without raw HTML.

  5. 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.

  6. https://github.com/laravel/framework/blob/10.x/src/Illuminate/Support/HtmlString.php

  7. https://github.com/laravel/framework/blob/10.x/src/Illuminate/Contracts/Support/Htmlable.php