This article is the product of a year of research on the newest server-side rendered (SSR) Javascript frameworks, focusing on Nuxt v3. It's intended for security enthusiasts, Nuxt enthusiasts and framework developers to show you what's possible and what to look out for in terms of security. I've documented my thought process, methodology and some common patterns I've observed.
Saying that I am interested in the security of Javascript Frameworks usually receives a sceptical "Why?". The assumption is that exploiting these frameworks is limited to simple and relatively dull issues. With massive attack surfaces comparable to huge frameworks like Spring and ASP.NET, the newest SSR frameworks introduce many attacks that were not feasible in the past. As the sophistication of these frameworks is increasing, their responsibility as the "front-end" is no longer valid, and disregarding their security could be incredibly hazardous.
I have spent the past year investigating various JS frameworks to challenge this assumption. Remote Code Execution, Arbitrary File Reads, and other exciting attacks are among the possibilities I have seen.
Nuxt is an older framework that allows you to create SSR websites with the power of Vue. The latest version is a complete rewrite of the framework, featuring fancy new features and updated technologies, such as Vue v3 and Vite v4.
The framework was suggested to me by a colleague during a front-end development task. At the time, Nuxt was in the Release Candidate stage and reasonably early in its development, and I had discovered a minor bug with the useFetch
function. While researching, I encountered a vulnerability that impacts data integrity due to a weak hash function.
From here, I decided to find as many vulnerabilities as possible, leading to 14 vulnerability disclosures across multiple repositories!
There are still plenty of bugs in Nuxt3, some of mine are still awaiting disclosure. Go find some 0 days!!
Those familiar with the Javascript ecosystem will know the node_modules
directory. This directory stores each dependency required by your project. Installing any large package guarantees you a 20GB folder filled with dependencies you've never heard of.
My favourites of these are the is-XXXX
packages. For example, the is-arrayish
package contains seven lines of code, has 15 stars on Github but has over 42 million weekly downloads.
It is one of the most popular packages on npm. However, a very tiny number of people know it exists, and an even smaller number know how it works! Given how widely these packages are used, security weaknesses at the base of npm can impact almost every package! This is prime security research territory.
'use strict;
module.exports = function isArrayish(obj) {
if (!obj) {
return false;
}
return obj instanceof Array || Array.isArray(obj) ||
(obj.length >= 0 && obj.splice instanceof Function);
};
Possibly the most famous package nobody has heard of.
One of the most exciting things about Nuxt is its from-scratch, minimal-dependency ecosystem Unjs. Unjs is designed to be a platform-agnostic set of tools used in Nodejs, the browser or a custom runtime like Cloudflare Workers.
While this is great for organisation and code reuse, the further down the dependency chain you go, the less popular the packages become. This is another example of good research territory.
A great example of this is immediately apparent. Nuxt depends on its bespoke web server Nitro, which depends on its bespoke web server H3. Simple vulnerabilities are unlikely to end up in the hugely popular Nuxt package, but a more petite package like H3 is guaranteed to be a softer target.
Using this to my advantage, I investigated the Unjs ecosystem. I found four vulnerabilities here, each of which could be used to impact Nuxt to varying degrees. These vulnerabilities relied upon straightforward techniques compared to the much more sophisticated vulnerabilities found in Nuxt.
Most of the vulnerabilities have demos on stackblitz, either find the demo link or click the file name on the code block.
Title Based XSS - Medium (6.1)
Static Code Injection - High (7.5)
Firefox Specific XSS - Medium (5.8)
Query String Injection - Medium (5.8)
Many different frameworks have a development
mode which allows you to make changes while the code is still running. These modes often have lots of user-friendly features. This provides a lot of attack surface to attack.
You might consider this cheating as it shouldn’t impact anyone. I mean, who exposes a development environment to the internet? Right? Right??
Even XSS on an isolated staging
subdomain could be a powerful tool for compromising another site. Development builds can still contain credentials or other secrets.
RCE in development mode - High (8.1)
Arbitrary File Read in development mode - High (7.5)
Semi-Arbitrary File Read in development mode - Medium (5.3)
Error Page XSS - Medium (4.7)
CVE-2023-0878 & CVE-2022-4414 are URL parsing bugs in Nuxt which impact static sites. These bugs are not very useful practically, but demonstrate a pattern in vulnerable URL parsing.
Nuxt attempts to preload a Link, but if it has a very specific href
value XSS can be triggered.
The link preload code works as follows:
// parseURL is a utility from the unjs/ufo library
// that does not depend on `new URL`
const parsed = parseURL(url_string);
await import("/" + parsed.pathname + "_payload.js")
This can be exploited if url_string
starts with a double slash as it will form a relative protocol allowing us to import a script from any origin!
The vulnerability was then patched, can you find the issue the following code?
const u = new URL(url_string, 'http://localhost')
if (u.search) {
return;
}
if (u.host !== 'localhost') {
return;
}
await import("/" + u.pathname + "_payload.js")
The issue is that the pathname
is still fully under our control! It is not commonly known but the new URL
API accepts relative protocols, allowing you to modify the origin of the URL.
// Result will be a URL pointing to https://attacker.com/path
new URL("//attacker.com", "https://bryces.io/path")
The URL //localhost//xss.bryces.io
will still satisfy all conditions and give us XSS! It took many further attempts to patch this issue as it can be tricky to detect when a path contains a relative protocol.
I’ve since learnt that import("./" + any_path)
is a suitable solution to this problem, just be aware of path traversals.
Lesson
new URL(user_input, constant)
in code, the resulting origin is not guaranteed to be the constant.new URL
is hard to get right.Thanks for reading, please send me feedback at [email protected].