Security Tip: When Is XSS Not Strictly XSS? (But Still Bad!)

[Tip #121] Technically, XSS involves injecting malicious Javascript, but sometimes you don't need any JS to get up to mischief! 😈

Security Tip: When Is XSS Not Strictly XSS? (But Still Bad!)

Great news! Alpine and Livewire are getting a CSP safe mode! Soon (v4!) you'll be able to run Livewire with your CSP and get rid of those pesky unsafe-*directives! 🎉 (I'm excited, can you tell??)

If you want to check it out early, here are the PRs:

Setting aside my excitement for a bit, when I was reviewing the Alpine PR, I was thinking about an interesting XSS attack vector:

If an attacker can inject HTML onto the page, can trigger existing javascript functions and perform unauthorised actions, without submitting any actual javascript!

It's XSS without the Scripting... or the Cross-Site... it's just an... attack? 🤷
Let's stick with calling it XSS for now, as far as I am aware there isn't a better name for it.

💡
This isn't a new idea, it's been around since frontend enhancements were a thing, and is definitely not unique to Alpine! Alpine just makes this easy to demonstrate.

Let's see this attack in action!

Traditional XSS

We'll start by first looking at how traditional XSS may look:

Injected inline Javascript

And if I click on Click me, I get this:

Boom! XSS fired.

This is easily blocked by a CSP that does not include unsafe-inline directive.

Content-Security-Policy: script-src 'self' 'strict-dynamic' 'nonce-*';

Clicking on Click me! gives us a violation and blocks the Alert:

"Refused to execute inline event handler because it violates the following Content Security Policy directive: "script-src 'self' 'strict-dynamic' 'nonce-Y_nzHVk5pxi8Evk4scqSBA'"."

Alpine Directives

Let's switch from raw Javascript to an Alpine directive:

<div x-on:click="alert('Boom!')">Click me!</div>

I've also switched my CSP over to Content-Security-Policy-Report-Only (because the CSP-friendly version isn't installed), so it'll still let javascript execute - but we will see all violations in the console.

Here's what happens when I click on Click me!:

No CSP violations thrown!

Nothing. 🧐

The CSP didn't detect the injected directive, and didn't report it. 😱

We've completely bypassed the CSP by using Alpine, instead of raw javascript, and Alpine & Livewire's new CSP safe mode isn't going to stop this attack.

This example is trivial, but consider what functions could be available in the frontend - admin controls come to mind! There was a fun attack in the WordPress world that abused an Add User javascript function to create a new admin account via an XSS payload on a public form. 😈

How do you stop this?

Don't render untrusted user input unescaped - always escape or sanitise that output. You can never trust user input, to make sure you always render it with as much paranoia as you can muster.

This attack works because HTML can do a lot of things in a lot of ways. If you need to let users submit HTML, run it through a parser and strip out everything that isn't essential. This includes the style directive!

Sometimes you do need to output user input where a frontend parser may consume it and perform actions. In this case, some frontend frameworks usually directives that can help: Alpine has x-ignore and Vue has v-pre - both of which instruct the framework to leave the HTML element untouched.

For example, if I add x-ignore to my page, the Click me! stops working entirely:

<div class="mt-6" x-ignore>
    <h2 class="text-lg font-medium">Stored value</h2>
    <div class="mt-2 p-3 rounded bg-neutral-100 dark:bg-neutral-800">{{ $stored }}</div>
    <h2 class="text-lg font-medium">Raw HTML</h2>
    <div class="mt-2 p-3 rounded bg-neutral-100 dark:bg-neutral-800">{!! $stored !!}</div>
</div>

x-ignore directive added on the top line


If you found this security tip useful? 👍
Subscribe now to get weekly Security Tips straight to your inbox, filled with practical, actionable advice to help you build safer apps.

Want to learn more? 🤓
Upgrade to a Premium Subscription for exclusive monthly In Depth articles, or support my work with a one-off tip or recurring Sponsorship! Your support directly funds my security work in the Laravel community. 🥰

Need a second set of eyes on your code?
Book in a
Laravel Security Audit and Penetration Test today! I also offer budget-friendly Security Reviews too.

Finally, connect with me on Bluesky, or other socials, and check out Practical Laravel Security, my interactive course designed to boost your Laravel security skills.