← More gists
Published

The security implications of packages in front-end apps

Third-party packages are great. They provide a lot of functionality and save us a lot of time. But what exactly are the security implications of using third-party packages in a front-end app (such as a React app)?

Many aspects play a role in the security of using a third-party package on the front-end;

  • unintentional security vulnerabilities
  • deliberately introduced malicious code
  • install scripts
  • bundling and transpilation
  • package (maintainer) trustworthiness
  • sub-dependencies

All of these also apply to front-end apps— to an extent.

Let's explore these aspects and their implications.

Unintentional security vulnerabilities

Unintentional security vulnerabilities reported in security advisories are the most obvious vulnerabilities. GitHub, Gitlab, npm, yarn, and many other tools provide automated scanning of security advisories. Most of those can be resolved fairly easily by updating to the suggested version.

Monitoring and resolving these is the least you can do, but don't fall into the trap of thinking this is enough.

Picture: A list of security advisories for a front-end project of various levels ranging from High to Low
Average security advisories for a front-end project
Aside

In my opinion the vast majority of front-end JavaScript security advisories are boring (i.e. unimpactful). A large majority are in development tooling, and unless they can be exploited via the network, they have zero impact. The rest are usually either ReDoS or only an issue if the package is used in a server context. There are very few vulnerabilities that actually matter for the front end.

Deliberately introduced malicious code

Deliberately introduced malicious code by a lone hacker, hacking group, or state hackers is the scariest kind of vulnerability. This is where someone adds malicious code to an otherwise useful package, turning it into a Trojan Horse. The right Trojan Horse could do almost anything; from hijacking your codebase and the data on your dev machine to installing ransomware or even hijacking all the user data on your servers.

Pure front-end apps are () immune to the worst of this, as they don't execute any privileged code on the host (your dev machine or server). There's still plenty to be careful of though, as a Trojan could still inject code into your bundle to hijack user cookies or keylog login and credit card credentials. Your cookies should be safe if you use HTTP-only cookies. Data theft through AJAX calls can be mitigated with a Content Security Policy (CSP), but it doesn't prevent JavaScript injection entirely (not even if you use nonces).

It's a different story when you're using the package within your or code, as then the package gets the same access as your server. Assuming your SSR/SSG backend has a database connection, that would probably mean access to all your data.

You wouldn't detect a really well-made Trojan Horse without thoroughly inspecting the package code. Someone smart will hide and obfuscate the malicious code, only run it in production mode, only for every 100th request, and only after two weeks from now (or something like that).

With the impact a Trojan Horse could have, by the time a security advisory has been published, it may have been too late.

Aside

Recently xz came in the news with a backdoor in an extremely popular Linux package. From what it seems, it was sheer luck that the right person found it at all. Hackers will learn from this, and next time we'll probably not be so lucky. (There's also some older history in the npm ecosystem)

Install scripts

Install scripts are an often overlooked danger of installing a new package. Any package can execute scripts after installation which can do pretty much anything you can do on your machine. Maybe it builds a C-executable for faster compilation, or maybe the script just installed malware.

This makes installing a package pretty much as risky as installing a random executable from the internet. What do you think would happen to your computer if you just installed every single application you ran into on the internet?

There's no reason not to be as wary of package install scripts.

Picture: Terminal output showing install scripts found in a front-end project. There are some strange entries (ljharb-monorepo-symlink-test), and esbuild is shown.
Some of the install scripts of a front-end project as reported by Can I Ignore
Tip

You can disable script execution by adding ignore-scripts true to your .yarnrc or ignore-scripts=true to your .npmrc, but this will probably be a bit more involved with certain packages.

Bundling and transpilation

Picture: Obfuscated JavaScript code of prettier-plugin-tailwindcss
An obfuscated bundled package

Most packages undergo bundling and transpilation (e.g. TypeScript to JavaScript) processes before publication. As a result, the code you get when you install a package does not match the code that is visible on GitHub. It would be very easy for a bad actor to sneak something extra in. These changes would be almost completely hidden, as they won't appear in a commit message, a diff, a GitHub release, the changelog, dependabot changes, or anywhere really.

The only way to see the real changes is by looking into the (often hard-to-read) code of the package as it was published.

Package (maintainer) trust

For most developers, package (maintainer) trustworthiness plays a big role in selecting what packages to use. If the package looks trustworthy and a lot of people use it, it's probably safe, right? Some go further and look into issues, how the maintainers respond to them, and other projects the developer has worked on.

A common rule of thumb I hear used is to only use packages with a certain amount of stars on GitHub. Unfortunately, star count isn't all that reliable.

We need better ways to determine trustworthiness.

Aside

Even if you were to check the trustworthiness of package managers when you first select a package, the maintainers of a package rarely stay the same forever. You would have to check again before every update. After all, it's encouraged to share maintainer access in Open Source projects.

Sub-dependencies

The majority of packages come with dozens of sub-dependencies, and the security implications applies to every single one of them. Even if you're going to review package code or (like the rest of the world) do a quick scan of the package and its maintainers, you'll need to do it for every single dependency in the chain, and especially the sub-dependencies. There's no better place to hide malicious code.

In conclusion

Every single front-end package, like any other kind of package, brings security risks. Some risks can be mitigated, such as through a Content Security Policy and disabling install scripts, other mitigations, such as analyzing maintainers and package code, would require such a tremendous amount of effort that they are usually too unreasonable.

The real solutions will need to happen at the platform level such as a module-level permissions system (experimental) and deny by default install scripts (RFC).

For now, I will be doing the following for my projects:

  • Automatic security advisory monitoring
  • Disabling install scripts
  • Setup a Content Security Policy
  • Use as few packages as possible
    • Copy over adding a dependency
    • Removing small, abandoned, and/or untrustworthy dependencies
  • Avoid obfuscated packages
  • Increasing the barrier to adding new packages
  • Reducing dependency update frequency

I reckon the only reason package security hasn't been a more common issue is because we're lucky most people online are still nice.

ps

Don't forget about your CI tooling. GitHub Actions, for example, automatically uses the latest version of actions by default. These can inject nasty stuff into your builds as well, and their code is even less visible.

More like this