17 Oct 2022

Aqua Nautilus has discovered that npm’s API allows threat actors to execute a timing attack that can detect whether private packages exist on the package manager. By creating a list of possible package names, threat actors can detect organisations’ scoped private packages and then masquerade public packages, tricking employees and users into downloading them.

This kind of attack is linked to a broader category of supply chain attacks. Over the past few years, Aqua Nautilus has seen an increase in the volume and variety of such attacks in the wild. This blog will dig deeper into this issue and demonstrate how users can mitigate the risks.

Timing attack to detect private packages on npm

Aqua’s research has shown that by using a timing attack a threat actor can detect the existence of private packages via npm’s API.

For instance, when an unauthenticated user is sending to the npm’s API a GET request (https://registry.npmjs.org/@<scope_name>/<secret_package_name>) to receive information about a private (scoped) package, the response is that this package isn’t found (HTTP 404 response), whether the package ever existed or not. The screenshot below shows how Aqua sent a request and received a “404 Not found” response (marked in red).

Aqua’s example shows how it can request API information about "secret packages, “a private package under the "random-organisation" scope using Postman. This request returned 404-Not found as the request came from an unauthenticated and unauthorised user. Additionally, it can see that the server responded after 686 milliseconds.

Caching mechanism

Aqua can assume that the flaw is embedded in the architecture of the API and is a result of the caching mechanism

If a threat actor sends around ~five consecutive requests for information about a private package and then analyses the time taken for npm to reply, they can determine whether the private package exists. More accurately, this would show whether the package exists or if it had existed in the past though has been deleted. In both cases, it would be the same result.

Due to this, Aqua can assume that this flaw is embedded in the architecture of the API and is a result of the caching mechanism. To validate that this flaw exists, Aqua conducted the following steps:

Creating a private package

As seen in the screenshot below, Aqua created a private npm package and uploaded it.

Aqua then used the organisation ‘random-organisation’ to upload the npm package ‘secret-package’. An authenticated user should easily be able to view this package including its name, while an unauthenticated user shouldn’t get any disclosure information about this package.

As the user can see below, Aqua verified the existence of this package with an authenticated user that belongs to “random-organisation” via the browser.

Executing a timing attack

Aqua compared the time it takes to search for a private package with a private package that doesn’t exist. For that, Aqua generated a single consecutive request. But Aqua didn’t find any significant differences.

From various systems, it started generating requests to receive private packages that did exist then compared the results with requests for private packages that did not. In doing so Aqua found a noticeable difference.

Optimising the timing of the attack

If user tries to replicate the exact results, there may be some differences due to connection strength and network speed

Next, Aqua collected and analysed the findings to optimise the timing attack. It was found that if it generated approximately five consecutive API requests as an unauthenticated user and looked for the new private package, it takes on average 648 milliseconds.

Yet, if Aqua generated about five consecutive API requests as an unauthenticated user to look for a private package that didn’t exist, it takes an average of 101 milliseconds. Consider that if the user tries to replicate the exact results, there may be some differences due to connection strength and network speed. Still, the results should be quite similar.

As the user can see in the graph and table above, it takes on average less time to get a reply for a private package that does not exist compared to a private package that does.

Supply chain attack via code packages

Threat actors often seek various ways to penetrate the organisation. Over the past few years, users have seen a dramatic increase by hundreds of percentage points in supply chain attacks. In some cases, the threat actors’ goal is to gain access to open-source packages/projects and poison them.

Bleeping Computer published a story about a supply chain attack in npm that impacted hundreds of websites and apps

Other times, they masquerade as private or public packages/projects, deliberately misspelling their names to trick unsuspecting victims into downloading their malicious package instead of legitimate, popular ones (i.e., installing the Python package Padnas instead of Pandas).

When this occurs, it’s not surprising that these incidents get wide coverage in the media. For instance, Bleeping Computer recently published a story about a supply chain attack in npm that impacted hundreds of websites and apps. In another report, they explained the risks of private package names exposure on npm.

What is npm scoped confusion?

When threat actors have additional information about the private package, like the timing attack described in this blog, they can better refine their attack. They can create a public package with the same name as the private package to mislead the developers into installing their malicious package.

On npm, a private package by the organisation ‘contso’ will be “@contso/contso-internal-package”. If it doesn’t exist as a public package, a threat actor that knows the name of the private package can create the public package “contso-internal-package”.

Scoped Confusion attack

In Scoped Confusion the victim actively downloads a public package instead of a private package with the scoped prefix

Aqua calls this Scoped Confusion attack. This is not to be mistaken for a Dependency Confusion attack where the victim automatically downloads a malicious package because of a flaw in the package manager.

In Scoped Confusion the victim actively downloads a public package instead of a private package with the scoped prefix. Moreover, creating a scoped package on the package manager is a mitigation step that reduces the risk of dependency confusion.

How attackers can merge everything into an attack

A Scoped Confusion attack usually starts with a threat actor who collects intelligence about a specific organisation:

A possible package names list

With this in mind, Aqua thought about a few methods that could be used to create a possible package names list:

  • Guess the names of the private packages used by a specific organisation by performing a dictionary or a guessing attack. Attackers may try to improve the dictionary list of specific organisations' private packages by looking for patterns or combinations in the organisations' public packages. For example, a  “contso” organisations might have public packages that begin with “@contso/contso -*, “@ contso/cnt-*”, “@ contso /core-*”. Prefixes like these can be used by an attacker to tweak his list.
  • Online public datasets (such as libreries.io) store historic information about packages. An attacker could search for public packages that were deleted since they may have been converted to private packages.
  • An attacker can map all the scoped packages on npm that don't have public packages, then create phony malicious packages with the same name. Additionally, attackers can use the npm API to map packages by average download per week to identify the most widely used packages. For example, a package called @graphql-codegen/visitor-plugin-common receives 2.2M downloads per week. However, there is no public package called visitor-plugin-common on npm. Thus, the attacker can create such a package to deceive users into installing it. It’s important to note that npm blocks users from creating and publishing public packages with the names of popular scoped packages, but this is not always the case.

Running a timing attack

Threat actors might tweak the algorithm to increase the chance of discovering an existing package

As the attacker has a potential list of scoped private packages, a timing attack could be generated. Threat actors might tweak the algorithm to make minor modifications in their package names list to increase the chance of discovering an existing package.

Once the timing attack has finished running, the threat actors would analyse the results, retaining the packages with higher average response times meaning that the private packages do exist.

Building public packages

Since the threat actors have created a list of possible private scoped packages, they need to check that there are no public packages (packages without a scoped) on npm with the same names, meaning they can create a malicious package under the "public" scope of npm.

Note that Aqua doesn’t encourage cybercrime. It merely describes here ways in which threat actors build their supply chain attacks.

Summary & mitigation

In this blog, Aqua has explained how we discovered a flaw in npm’s API which is disclosing information about organisations' private packages. Threat actors can create a list of potential private package names and run timing attacks to verify their existence.

Later, threat actors could create public packages masquerading as legitimate private ones and trick unknowing developers into downloading malicious packages. Aqua has disclosed this information to GitHub which, in response, replied that this architecture of the API is by design. “Architectural nuances prevent us from systematically preventing timing attacks from determining whether a specific package exists.”

Following are some steps users can take to mitigate these risks:

  • Gather a list of all the organisation’s private and public packages on all the package management platforms.
  • Actively look for typosquatting, lookalikes, or masquerading packages. Verify that there are no other packages with the same name as the internal private packages. 
  • If users find any similar packages, make sure that they do not contain malware and notify the relevant stakeholders.
  • If users don’t find public packages similar to internal packages, consider creating public packages as placeholders to prevent such attacks.

The timeline of the discovery:

  • 03-08-2022: The issue was reported to GitHub’s bug bounty program at HackerOne.
  • 03-25-2022: GitHub triaged and responded: “Because of these architectural limitations, Aqua cannot prevent timing attacks from determining whether a specific private package exists on npm”- Furthermore, they exclude timing attacks from the bug bounty programme.