CSS injection using component islands and useHead in nuxt/nuxt
Reported on
Feb 6th 2023
Description
After a component island render, the resulting head is regex'd for tags. This regex is not very robust and can be tricked, allowing for CSS injection.
Proof of Concept
app.vue
<template>
<div>
Nuxt 3 Playground
<Page :title="title" />
<input v-model="title" type="text">
</div>
</template>
<script setup lang="ts">
const title = ref()
</script>
nuxt.config.ts
export default defineNuxtConfig({
experimental: {
componentIslands: true
}
})
page.server.vue
<template>
<div>
hello!
</div>
</template>
<script setup lang="ts">
const props = defineProps<{title: string}>()
useHead({
meta: {
name: 'description',
value: props.title ?? 'Hello!'!
}
})
</script>
Using the payload:
><style>*{color:red}</style>
This payload works as <
and >
within attribute values are interpreted as closing and opening tags.
The impact here appears to be limited to CSS injection, it could possibly be raised to XSS but I could not figure this out as double quotes cannot be used.
Impact
CSS injection is mostly useful for defacement, but can also be used to steal information (see CSS keylogger).
Including user provided data within metadata is very common!
References
Try this in production mode if you're having trouble reproducing
This issue doesn't work on the new useHeadSafe
function, but still works on the old function.
I think this is the purpose of useHeadSafe
- sanitising user input. Wouldn't this also be a problem with any user-generated content in head?
e.g. ><script>console.log('hi')</script>
So useHeadSafe
does sanitize input and it prevents users from escaping tags (like the previous title XSS bug). useHead
also now does this for title, and it also does this for meta.
For example you cannot do "><script>blah" in our example because useHead
is escaping the quotes within meta tags.
The issue occurs in Nuxt islands when this regex is used to parse the input back.
Essentially anywhere that you can place <
or >
characters without them being escaped will then be parsed by Nuxt Islands and either a link
or a style
can be injected.
As you know Vue will escape <
and >
into >
and <
to prevent XSS, however useHead
only escapes "
within attributes, as, in theory, you cannot perform any injection if you cannot escape the quote.
For example:
useHead({
meta: {
name: 'description',
value: 'test <>"'
},
});
The output of this will be:
<meta name="description" value="test <>""
There is no injection bug here, but when combined with the issue in Nuxt Islands it is then possible to inject a script, as any time <style>
is present within the head it will be injected.
While useHeadSafe
does mitigate this issue, I don't think it makes much sense to provide useHead
which does the same things but is vulnerable in a specific edge case of use. If this is the case I would recommend that the function is renamed to useHeadUnsafe
as you cannot trust this function.