# Your npm Dependencies Can See Everything. Here's How We Lock Them Down! — Kaloyan Kosev | Ambire

- Channel: [ETH Belgrade Community](https://streameth.org/eth-belgrade-community)
- Date: 2026-10-06
- Duration: 20:48
- Topics: People & Blogs
- Watch: https://streameth.org/watch/yt-LQjIoL6_T-0
- YouTube: https://www.youtube.com/watch?v=LQjIoL6_T-0

## Transcript

Hello, everyone. I hope you can hear me well. Uh perfect intro. This This is actually a perfect intro, so I can deep dive directly into the topic that I would like to share. Uh and yeah, I'll start by saying that whenever first starts with JavaScript, I think one of the surprising thing one found finds is just inserting a script or any NPM dependency kind of gives an access to all your code base. So, it's a frequent case nowadays that people review only the dependencies that are direct towards the project. In this example, 184 are our direct dependencies, but we actually ship 2,049. And why is that? You all know why, because every dependency has a dependency that has a dependency. So, down the sub-dependency tree, the number of the code that gets bundled in your in your application is quite quite a lot. And no one can review carefully and hand-by-hand all the 2,409 dependencies on our end. And that's why I will share with you how we manage actually having so many third-party dependencies while having sensible resistant to supply chain attacks architecture. But before that, let's start by the attack vector. So, if you followed the supply chain attacks recently, usually the attack vector could be explained with this piece of code. You see, we have a dependency of a dependency of a dependency that got published just recently, maybe a couple of hours ago. And take a look at this code. This is a standard JavaScript. JSON.stringify is a built-in method. And what this code does, it overrides the built-in Java stringify JSON stringify. And the devil is in the details. It executes some logic in this example, fetch, which sends a request when it detects a seed phrase. It sends a a request towards the attacker server. And then it returns the original the original behavior of the JavaScript JSON stringify. Uh the This is not hypothetical. This is not theoretical. This is exactly how some of the supply chain attacks got got executed. And why why this works is the first question. Well, it's it's just how JavaScript works. Every dependency runs with full authority on your code base by default. And you uh probably the developers among you all know that in JavaScript importing is not a permission, it's a merge. And then NPM currently has no way to say this package cannot use fetch or cannot use JSON stringify. So, uh in this uh very very uh I'll say liberal world, uh what we can what we can do to defense? Well, in Umpire, uh we uh implemented a concept uh called uh Secure ECMAScript and a package uh called LavaMode. Uh it still surprises me after so many years in the industry that many of the senior engineers that are working with JavaScript day-to-day still haven't um heard of uh Secure ECMAScript. And uh why is that? Well, uh Secure ECMAScript is still not standardized in JavaScript. So, it's a proposal. Um and one way to have secure ECMAScript nowadays is to use a shim or a package that implements the concept, which is yet to be standardized. Um and uh this is exactly uh how uh how we did it on our end. So, secure ECMAScript is a package that has a few cool things. Uh one of them is walkdown. What a walkdown does is basically it freezes all the built-ins. Imagine JSON.stringify from the previous example or uh fetch. So, all these built-ins uh uh become read-only for all your dependencies. Um usually, there is uh almost no real use case. Now, I say almost because there are some libraries that are adding some things to the built-ins in JavaScript, but usually a dependency shouldn't be able to override some most of these built-ins. And then, uh on top of that, we add LavaMode. LavaMode basically is a policy on top of another concept in secure ECMAScript called compartments. So, compartments are still a proposal. And what is the core idea uh is every dependency has a compartment of what it can touch, what it can see. And uh if uh let's say you define a null scope for a dependency, and most of the dependencies can't um touch JSON.string JSON.stringify. Yeah. So, in an essence, uh these two things give gives us um the um supply chain security, the whole supply chain security that we have. And yeah, uh another uh uh another way to visualize it is that a CS is freezing the shared built-ins, and then LavaMode gives each packet its its own room with specific set of things that it can do. Um what a compartment actually is uh is um very simple. Uh you see this convention with, and there is a scope proxy, and then the module, the dependency, lives uh in this own uh scope only, and access only the things that you give it. So, it sees only the globals you uh listed for it, and everything else is practically undefined. So, if it tries to override um fetch, it fetches undefined in the dependency uh in the dependency scope. And the policy uh I spoke I talked about uh before that, the policy is just a a list. It's a it's a JSON. Uh this is an example with one of the packages that we currently have, VM, and you see it lists all the globals. So, VM is able to access fetch, request, set timeout, and etc. And also, uh one nuance here, it lists all the sub-dependencies that it is able to interact with. This way, uh third-party dependency cannot interact with any third-party dependency down on your dependencies list, which adds uh another uh layer of security. And yeah, this is how um the representation of a sub-dependency uh sub-dependency looks like. And yeah, in the globals, it's also worth noting that a dependency can have only a subset. Uh see this crypto.getRandomValues, So, the the the sub-dependency in this example can only access a sub part of um the crypto built-ins. And yeah, that's it. That's the security model. Uh I can also show you a package with almost nothing. This is the big number js sub-dependency of the grid plus SDK that we're having. Um and uh this is uh actually very intuitive because big number js is a library that makes math on your money. It can't make It shouldn't make network requests, and it doesn't make any sense. In its compartment actually, as I said, fetch is completely undefined. So, moving forward, I can show share with you our uh real uh policy. So, I generated with AI uh this uh cool uh graph. Uh we have uh 319 uh packages that currently in our production bundle. And if we didn't have compartment at secure ECMAScript, uh all these 319 packages that land in our production bundle could access uh dangerous things uh alongside uh calling uh local storage, fetch, navigator.hit, uh and uh other um sensitive APIs in our uh project. Uh we are working on a web 3 uh wallet, as you know. So, uh having this secure is very very very much critical, but I would argue that in any dapp or any app, uh this is equally critical uh to be able to restrict uh what the packages can see and do in your in your production bundle. And with our configuration, uh on the contrary, uh here is uh how uh the same chart looks like. Notice that a vowel and function, none of our 319 dependencies can see or override these. Same as indexedDB. In our whole bundle, there is only two packages that can call Chrome. And Chrome is very dangerous because uh Chrome APIs and browser extensions, they um are the way to access the extension storage where we uh store encrypted our user uh secrets. So, in our whole bundle, only two applications can actually see that we have a storage, and these two packages are packages that are doing something specifically to the um to these APIs, and they need to to see it and access it. Um yeah, and with fetch, only 13 packages uh in our case, um only a small portion of them can use and see fetch, and as I showed, VM is one of them because it makes uh network request for the RPC communication and stuff. And this also is a very good for analytics. When we first implemented with our colleagues, we were very uh very uh curious uh why this package has access to uh let's say the local storage. And this process was really really interesting because it boils down the checks to one or two packages that access critical infrastructure uh in our case. So, yeah, uh very very enlightening. So, now, let's kill the attack from the second slide that I showed you. If a compromised package like this lands in our bundle, what would happen? First of all, JSON.stringify trying to override a built-in for a package that doesn't have this permission, it will throw an error that the JSON is frozen. This is type error, so the application will crash even if we this package lands on in our bundle. And we have a second gate, which in this case will not click because it's a type error, but uh the second gate is the fetch that I showed you in the example that uh in the middle when we call JSON.stringify is uh getting the user seed phrase and sending it to the attacker server. This fetch will also fail with a type error because of the policy thing that I explained. So, we have a two uh very good uh gates uh for the supply chain security that we have configured and uh an attack similar to this uh would uh be very difficult to happen uh in our case or that at least the attack surface is very small percentage of the dependencies that actually use these APIs. Moving on next, we have some extra final time to talk about the other attack vector, which is very popular in the recent supply chain attacks, and it's the install time attack. So, install time attack is a different beast. Uh it happens even before you start or deploy your application. You know, on your laptop or on the CI, um you have in the dependencies uh this uh post-install hooks. And the post-install hooks basically can execute arbitrary code in your terminal. So, you type npm install, and if a subpackage has this arbitrary uh this post-install hooks, uh it can basically do uh whatever you do in your in your terminal. And uh this is uh exactly uh how some of these supply chain attacks uh happened. Uh imagine uh the shai hulud case in September last year. What happened is post install hook of infected package was scanning for secrets on your developer machine. And not only that, but it also harvested NPM credentials. And if you are developer yourself and have some packages in NPM, it is distributing the same attack vector on your packages. So, it's like a a worm. It's like a How should I say? Not It's a a very a troublesome for you as a developer even before your infected package lands. Or if it doesn't land to the production, it's it's it's still very bad. In our case, with LavaMode, we have this concept called install scripts policy. So, in our case, we just have a couple of packages, I think less than five, that we have allowed post install scripts. And only for packages that actually need to do something with this post install script, which is important for our use case for the for the wallet. Because we have many packages that execute install scripts for things that we simply don't support. So, we don't need these install scripts. And yeah, this is basically configured for less than a day and it saved us for every single supply chain attack, which was related to install scripts. Last but not least, I would like to share a couple of gotchas because it all sounds very simple on theory, but of course, when you start working on this, there are some exceptions and hard things to to discover. So, one exception which we very quickly found is that we simply cannot sandbox everything. In our case, it doesn't make sense to sandbox all of our content content because this in the browser extension world are getting injected into the websites to the daps and therefore are unsecure by presumption. So, we tried to sandbox them as well, but it introduced more bugs than pros and we just decided to that some parts cannot be secured and shouldn't be secured. Our focus should be in our service worker in our core um extension bundle. Next, got chase the performance. Well, these compartments had some performance problems because every package gets um harder way to communicate, let's say, out of this compartment. So, one of these packages was React Fast Compare. Maybe the developers among you have heard about this package. For this package, it is essential to be speedy and we found that for this package when we added to a compartment, we lose most of the speed optimizations that it brings. This was found with trial and error. And for this package, we just wrote an exception. We are not adding it into a compartment, but we configured another mechanism on our CI when this package gets version update, we very carefully check its dependencies only. But it's still better because as I said, out of the 200 packages, we just have one or two which we need to manually check. And we are also scanning them with our third-party providers for supply chain security. So, yeah, some of packages just needs to be escaped excluded excluded from this configuration based on the on your need. And I will finish by saying what LavaMod and Secure Atmospheres cannot guard. Well, they're not perfect security. As I said, if there's a compromised package which in your policy you already gave him permission to some APIs, it can still slip. But as I said, the attack service is is a lot a lot smaller. And yeah, in some of these supply chain attacks, most of the packages that were affected was small utility package which does one specific thing and overrides a built-in like I showed. So, yeah, for the packages that we have a lot of permission like VM ethers, for these packages too needs to be inspected very carefully. And um There is another quick win for these packages. We set a policy for not accepting packages which are um were published in the last 14 days. So, this is sounds very simple. There are some package managers that even support this in their configuration. And just by excluding packages that have been deployed recently, you cover a lot a lot in your your app becomes a lot a lot more more secure. And for this talk to be also practical amongst the developers among the developer from you, I'll share two things that you can do on Monday on your code base that are less than one day of work and can harden your security magnificently. So, one of these things is like I said, block your install scripts. It You will see for some projects that I blocked install scripts, I found that none of the dependencies that are actually using any. So, blocking your install scripts by default gives you a big big, uh, security hardening. And the second thing is age gate your dependencies. With NPM, there's still currently no mechanism, uh, I think. But with other package managers like PNPM, there is. It's a simple config. In our case, we just wrote a CI, uh, and the CI is going through the lock file to all the dependencies and subdependencies and sub-subdependencies. And this, uh, CI is basically checking, uh, was this, uh, dependency published, uh, earlier than 14 days ago? If not, the CI just fails and we, uh, wait. Because most of these attacks are getting found in the first hours up to a day. And therefore, having, uh, offset with a few days is, uh, yeah. Uh, reduces the attack, uh, attack vector a lot. Good. Well, uh, this was my talk. It's almost perfect 20 minutes. I don't know how I did this. So, uh, if there is no time for questions, uh, I would encourage you, if this topic's interesting for you, uh, to follow me on X. Uh, since the previous week to, uh, warm up my little audience, uh, on this topic, I wrote, uh, three, uh, very detailed, uh, tweets uh, explaining the concepts in this presentation. So, you can find, uh, in, uh, great, uh, tweet style details, um, all the core, uh, bits of my presentation. And I would love to chat with you on this, uh, topic, uh, here as well. Thanks so much. &gt;&gt; Thank you.
