If you had relied solely on `package-lock.json`,
Why the Shai-Hulud npm Supply Chain Attack Is So Alarming

If You Were Relying Solely on `package-lock.json`: Why the Shai-Hulud npm Supply Chain Attack Is So Alarming
A while back, while browsing Reddit’s r/devsecops, I came across a post that really caught my attention.
It described how, in early August 2026, starting with keyvand cacheable, another large-scale supply chain attack had occurred in the npm ecosystem.
The gist of the post was roughly as follows.
We’re still running `
npm install` in our CI pipelines without a second thought.
And this time, it wasn’t just the security industry trying to scare us.
In fact, on August 4, 2026, the legitimate distribution channels for widely used packages in the keyv and cacheable families were exploited by attackers, and self-replicating malware from the Shai-Hulud family spread to hundreds of npm packages. Although the numbers varied slightly depending on the time of the investigation, JFrog identified more than 400 packages and more than 1,700 versions, and while the Cybersecurity Agency (CSA) of Singapore explained that an ecosystem comprising over 1,300 malicious package versions—accounting for approximately 2 billion monthly downloads—had been compromised.
This is where the problem begins.
The compromised packages were not ones with suspicious names like “free-bitcoin-generator-2026.”
They were legitimate open-source packages that we might commonly encounter somewhere in a dependency tree.
1. How did this attack begin?
The attacker took control of the GitHub account managing keyv and related packages, then injected malicious code into the legitimate repository and release process.
Notably, the malicious keyv@6.0.0file contained code (`preinstall`) that executed upon installation. In other words, the application didn’t even need to call `import keyv`.
The attack could begin the moment the dependency was installed.
npm install
While this may seem like a routine command to a developer, from the package’s perspective, it also presents an opportunity to execute a lifecycle script during the installation process.
The malicious payload collected various credentials from the developer’s PC and CI/CD environment.
The targets included not only GitHub tokens and npm tokens but also cloud credentials, SSH keys, and Kubernetes and Vault-related secrets—all the kinds of credentials a developer might have in their work environment. It then used the npm publish permissions it had obtained to re-infect other packages managed by that developer, spreading on its own.
That is why this attack is referred to not as a simple credential stealer, but as a worm—that is, a self-replicating supply chain attack.
To simplify the process, it goes like this.
Maintainer 계정 탈취
↓
정상 GitHub Repository 변조
↓
악성 npm package 배포
↓
개발자 / CI에서 dependency 설치
↓
credential 탈취
↓
npm publish token 확보
↓
피해자가 관리하는 다른 package 감염
↓
다른 개발자가 다시 설치
↓
반복
This is precisely why supply chain attacks are so dangerous.
The attacker does not need to hack every developer directly.
They simply need to hack a single point that developers already trust.
2. What makes it even more troubling: You don’t even have to install keyv
“But I don’t use Keyv, do I?”
This is one of the most dangerous ways of thinking during a supply chain incident.
JavaScript projects have far more transitive dependencies than directly installed ones.
For example, a project might end up with a dependency chain like this:
eslint
└── file-entry-cache
└── flat-cache
└── keyv
This means that even if I only installed ESLint, `keyv` could be included several levels down in the dependency tree.
This is why, even on Reddit, people have been saying since this incident that you need to check packages you don’t even use directly. To determine whether there was actual damage, it was necessary to verify not just the package names, but exactly which versions were present in the dependency tree.
The verification process itself is simple.
npm ls keyv
npm ls cacheable
npm ls flat-cache
npm ls file-entry-cache
Alternatively, you can search the lockfile.
grep -n '"keyv"' package-lock.json
Of course, this doesn’t mean you can check for the entire campaign based on this alone. In reality, you must check against the official IOCs and the list of affected packages released at the time.
3. So, doesn’t having a `package-lock.json` file mean you’re safe?
There’s a rather significant misunderstanding here.
To put it simply:
The lockfile is important. However, it is not supply-chain security in and of itself.
package-lock.jsonIf you have this file and use `npm ci` in your CI, the risk of a fixed dependency tree suddenly being replaced by the latest malicious version uploaded today is significantly reduced.
This is certainly a good defense.
But let’s consider this scenario.
1. 정상 버전 5.9.0 사용 중
2. 공격자가 6.0.0 배포
3. Renovate / Dependabot / 개발자가 6.0.0 update
4. CI 통과
5. package-lock.json에 6.0.0 기록
6. merge
What can a lockfile do in this case?
It will install the malicious version very reliably and reproducibly.
npm ci
It accurately reproduces the exact same malicious dependency tree every time.
The lockfile’s role is fundamentally to lock in dependency resolution—not to
not to prove that “this code is safe.”
"This code is safe."
The same applies to the integrity hash.
A hash verifies
"The file I received is identical to the one uploaded to the registry."
.
But what if the file uploaded to the registry is malicious itself?
It would essentially be accurately verifying the SHA-512 hash of the malware.
4. There was even provenance
Personally, this is the most interesting part of this incident.
Generally, package provenance or trusted publishing is used to strengthen supply chain security.
The idea itself is reasonable.
Instead of
개발자 노트북
→ npm token
→ npm publish
go through personal credentials,
GitHub Repository
→ GitHub Actions
→ OIDC
→ npm Trusted Publishing
the goal is to ensure that packages follow a verifiable distribution path.
However, some of the malicious releases related to the “keyv” incident had even passed through legitimate GitHub Actions-based trusted publishing and possessed valid provenance.
It wasn’t that the distribution system was compromised;
but because the source had already been compromised before the distribution system executed it.
This incident highlights an important distinction.
Provenance가 증명하는 것
"This artifact came from this repository/workflow."
But what we really want to know is this.
"This artifact is safe."
These are not the same thing.
While a valid signature and provenance can prove the reliability of the distribution path, they do not prove that the code itself, which traveled along that path, is benign.
5. Claude Code and VS Code Have Also Become Targets
There is one aspect of this variant that is particularly characteristic of 2026.
The attackers didn’t stop at simply scanning .npmrcor .env.
According to analyses by some security firms, the attack also included persistence mechanisms utilizing Claude Code configurations and VS Code tasks. It was designed so that the very act of a developer reopening a project or using an AI development environment could serve as an execution point for the attacker.
Until just a few years ago, these were the types of files considered the most sensitive on a developer’s PC.
~/.ssh/
~/.aws/
.env
.npmrc
Now, it seems we need to add one more to that list.
AI coding agent configuration
As agents like Claude Code, Copilot, and Cursor gain access to an increasing number of files, shell commands, and GitHub repositories, AI development environments are also becoming a new supply-chain attack surface.
6. A question from Reddit: Why do we still run all scripts in CI?
Following this incident, a point was raised on r/devsecops that resonated with many.
It pointed out that many teams still allow the execution of installation scripts by default when fetching new dependencies in CI.
Typically, CI configurations look like this.
- run: npm ci
- run: npm test
- run: npm run build
The problem is that CI runners are much better targets for attacks than one might think.
CI environments often contain the following:
GitHub token
AWS credential
Docker registry credential
npm token
deployment secret
SSH key
And depending on the situation, the dependencies we run may have these permissions.
install되는 순간 JavaScript 실행
We had placed both of them in the same environment.
7. At the very least, we need to reevaluate our policy regarding install scripts
npm has an option to prevent the execution of scripts during the dependency installation process.
npm ci --ignore-scripts
ignore-scripts=trueSetting it to `package.json` prevents the automatic execution of the `install` lifecycle script defined in ``. Recently, npm has also introduced the `allowScripts` policy, which allows only the `install` scripts of required packages.
Of course, in reality, not every project is that straightforward.
This is because there are dependencies that actually require an `install` script, such as those involving native modules or binary downloads.
Therefore, a more practical approach is
모든 script 허용
to
필요한 script만 허용
to
This is the same reason why allowlists frequently appear in security contexts.
8. Not installing new versions immediately is also a defense mechanism
There’s also a somewhat interesting defense strategy.
It involves excluding a package from the list of potential installations for a certain period immediately after it is released.
Currently, npm has a `min-release-age` setting.
For example:
# .npmrc
min-release-age=7
This ensures that newly released versions won’t be included in dependency resolution candidates until at least 7 days have passed. The official npm documentation also supports this.
Why does this work?
Because a significant portion of supply chain malware relies on this very vulnerability.
악성 package publish
↓
개발자 install
↓
보안업체 발견
↓
registry에서 삭제
If it takes two hours for an attacker to distribute a package and for it to be detected,
I simply need to avoid installing the new version during those two hours.
Of course, this won’t work against zero-day exploits or malicious versions that lie dormant for a long time.
Still, unless your service absolutely requires real-time dependency updates, this is a fairly inexpensive layer of defense.
9. If it’s already installed, is deleting just `node_modules` enough?
No.
This point is particularly important.
The GitHub Advisory Database recommends treating systems with the malicious `keyv@6.0.0` and related packages installed as fully compromised.
This is because simply removing the package does not guarantee that all malware has been eliminated.
In other words, incident response
rm -rf node_modules
npm install
is not an issue that can be resolved at that level.
If an actual malicious version was executed on a development PC or CI runner,
GitHub credentials
GitHub credentials npm credentials
cloud credentials
SSH key
CI/CD secrets
Incident response must be conducted on the assumption that these and other credentials may have been compromised.
Furthermore, it is safer to perform credential rotation in a trusted, clean environment rather than on an infected machine.
10. Ultimately, what we trusted was not the package itself, but the “chain of trust.”
In npm,
npm install some-package
is not actually about trusting a single person.
We are, in fact, placing our trust in quite a few things at once.
Maintainer
↓
GitHub account
↓
Repository
↓
Pull Request
↓
GitHub Actions
↓
OIDC / Publish credential
↓
npm Registry
↓
Dependency resolver
↓
Lifecycle script
↓
CI runner
If an attacker takes control of even one of these, problems can arise.
And in modern JavaScript development, the number of dependencies ranges from hundreds to thousands.
Ultimately, the core principle of supply-chain security is
"Do not trust."
but rather
"Do we know exactly what we’re trusting?"
.
Things I’ll be checking in CI following this incident
Personally, I plan to re-examine at least the following points.
[ ] CI에서는 npm install 대신 npm ci 사용
[ ] lockfile 반드시 commit
[ ] dependency update PR의 transitive dependency 변경 확인
[ ] install lifecycle script 최소화
[ ] 가능한 환경에서 --ignore-scripts 적용
[ ] 필요한 install script는 allowlist 방식으로 관리
[ ] 신규 package version에 release-age 정책 적용 검토
[ ] CI credential 최소 권한화
[ ] npm / GitHub publish credential 최소 권한화
[ ] dependency update와 production deployment 분리
[ ] 개발 PC와 CI에서 secret 노출 범위 최소화
[ ] AI coding agent configuration도 보안 검사 대상에 포함
And one more thing:
npm audit
Don’t let your guard down just because you passed.
This incident wasn’t the typical vulnerability where a single CVE is created and exploited when a specific function is called.
The package itself was malware.
Conclusion
Modern development is virtually impossible without open source.
Therefore, the solution is not to eliminate dependencies.
Instead, dependencies must be treated as executable external code.
We would naturally be wary if told to immediately execute an arbitrary shell script downloaded from the internet on a server.
But strangely enough,
npm install
that wariness largely disappears in this context.
The recent Shai-Hulud incident has very clearly demonstrated that this distinction does not actually exist.
package-lock.jsonIt is also necessary.
Provenance is also necessary.
2FA is also necessary.
Trusted Publishing is also necessary.
However, none of these measures alone
safe = true
on its own.
Ultimately, supply chain security is not a single, perfect defense;
but rather the process of establishing as many trust boundaries as possible that an attacker must pass through.
Furthermore, going forward, we should view the `npm install` command as far more security-sensitive than we previously thought.