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! 😈

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.
Let's see this attack in action!
Traditional XSS
We'll start by first looking at how traditional XSS may look:

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

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:

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!
:

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.