Tinycolor Supply Chain Attack Post-Mortem
Posted4 months agoActive4 months ago
sigh.devTechstoryHigh profile
heatednegative
Debate
80/100
Supply Chain SecurityNpmCi/cd
Key topics
Supply Chain Security
Npm
Ci/cd
The Tinycolor supply chain attack highlights vulnerabilities in npm package management and automated publishing workflows, sparking discussion on improving security measures.
Snapshot generated from the HN discussion
Discussion Activity
Very active discussionFirst comment
26m
Peak period
48
0-6h
Avg / period
8
Comment distribution80 data points
Loading chart...
Based on 80 loaded comments
Key moments
- 01Story posted
Sep 17, 2025 at 1:18 PM EDT
4 months ago
Step 01 - 02First comment
Sep 17, 2025 at 1:44 PM EDT
26m after posting
Step 02 - 03Peak activity
48 comments in 0-6h
Hottest window of the conversation
Step 03 - 04Latest activity
Sep 20, 2025 at 2:42 PM EDT
4 months ago
Step 04
Generating AI Summary...
Analyzing up to 500 comments to identify key contributors and discussion patterns
ID: 45278657Type: storyLast synced: 11/20/2025, 8:52:00 PM
Want the full context?
Jump to the original sources
Read the primary article or dive into the live Hacker News thread when you're ready.
> A new Shai-Hulud branch was force pushed to angulartics2 with a malicious github action workflow by a collaborator. The workflow ran immediately on push (did not need review since the collaborator is an admin) and stole the npm token. With the stolen token, the attacker published malicious versions of 20 packages. Many of which are not widely used, however the @ctrl/tinycolor package is downloaded about 2 million times a week.
I still don't get it. An admin on angulartics2 gets hacked, his Github access is used to push a malicious workflow that extracts an npm token. But why would an npm token in angulartics2 have publication rights to tinycolor?
Imo, this is one of the most classical ways organizations get pwned: That one sin from your youth years ago comes to bite you in the butt.
We also had one of these years ago. It wasn't the modern stack everyone was working to scan and optimize and keep us secure that allowed someone to upload stuff to our servers. It was the editor that had been replaced years and years ago, and it's replacement had also been replaced, the way it was packaged wasn't seen by the build-time security scans, but eventually someone found it with a URL scan. Whoopsie.
I wonder if someday we'll find there's also a more active process, which resembles "remove old shit because it may contain security vulnerabilities."
I had just about convinced myself that we should be using a GitHub action to publish packages because there was always the possibility that publishing directly via 2FA, that one (or specifically I) could fuck up and publish something that wasn’t a snapshot of trunk.
But I worried about stuff like this and procrastinated on forcing the issue with the other admins. And it looks like the universe has again rewarded my procrastination. I don’t know what the answer is but giving your credentials to a third party clearly isn’t it.
The OP gave the GH repo too broad permissions. There is no good reason for the repo CI workflow to have full access to everything under their account.
I think over the last few weeks I have at least talked myself into going back to maintaining multiple user accounts on my laptop to separate personal, open source, and entertainment into separate accounts to reduce the last radius, but the fact is sometimes I like to do two things at once and that will be a pain.
Fresh git checkout on prod publish. Run all npm/node commands in ephemeral rootless containers. Only have publish token exposed and injected when you are actually publishing (not on install/build just prior). Separating users like you mentioned doesn't hurt bt doesn't sound like your lower-hanging fruit nor something that would likely save your bacon on its own without other workflow adjustments.
None of this is relatively difficult per se, just a bit extra friction which should be worth it to avoid these kinds of events.
Packages which don't have approval and review by a reliable third party shouldn't be visible by default in a package manager.
In the meantime, I'm trying to do my part through occasional random spot inspections when there's an update to a package, and encourage others to do the same for swarm coverage.
In any case, if the choice is “frequent supply chain compromise, take it or leave it”, the answer is of course “leave it”.
If we need to pay for curated packages because the problems with NPM are endemic, that’s not unreasonable.
Yeah, there's that insane entitlement. More demands for others' time and labor, plus the conflation between you demanding labor vs if people don't agree to your free labor demands, they're pro supply chain compromise.
There's another choice: vendor your dependencies and manually review and vet updates. That solves all your problems, no need for "trusted third parties", you are the one vetting it, only need to trust yourself.
Fix it early so the user does not have to deal with the complexity is most often the best approach.
(right now I don't know the answer to that for the stuff I'm responsible for, but I'm in the process of researching and setting up and configuring the sort of tools needed to automate that.)
I once heard from a sysadmin that didn't want to automate certificate renewal and other things, because he believed that doing so would take away useful skills or some inner knowledge of how the system works. Because of the human error risk, I thought that was stupid, but when it comes to approval processes, I think it makes sense. Especially because pushing code doesn't necessarily mean the same thing as such an approval, or the main device that you push code from could also get compromised, using your phone as 2FA could save you.
Then again, maybe I'm also stupid and the way we build our software is messed up on a fundamental level with all of the dependencies and nobody being able to practically audit all of the code they import, given deadlines, limited skills and resources and so on. Maybe it's all just fighting against a windmill.
Ongoing downstream review of all dependency code is practical for only a tiny fraction of projects; for most projects using publisher reputation as a proxy for package safety is reasonable.
What’s not working is the low-standards package managers where inconveniencing authors is never acceptable because the whole enterprise is built on popularity with authors — you can’t trust that what those package managers give you reflects author intent.
People like to complain about distribution packaging being obtuse, but most distributions have rich support for verifying that package sources were signed by a key in a keyring that is maintained by the distribution. My (somewhat biased) view is that language package managers still do not provide the same set of features for validation that (for instance) rpmbuild does.
The release process for runc has the following safeguards:
Maybe there are still gaps in this setup, and I would love to hear them. But I think this setup would have blocked this kind of attack at several stages. I personally don't like the idea of signing releases in CI -- if you really want to build your binaries in CI, that's fine, but you should always require a maintainer to personally sign the binaries at the end of the process.For language package managers that do not support such a workflow, trusted publishing is a less awful setup than having long-lived publishing keys that may be incorrectly scoped (as happened in this case) but it still allows someone who gains access to your GitHub account (such as by stealing your cookies) to publish updated versions of your package with very little resource. GitHub supports setting a mandatory timeout for trusted publishing but the attacker could easily disable that. If someone got access to my GitHub account, it would be a very bad day but distributions would not accept the new releases because their copy of our keyring would not include the attackers keys (even if they added them to my account).
Disclaimer: I work at SUSE, though I will say that I would like for OBS to have nice support for validating checksums of artefacts like Arch and Gentoo do (you can /theoretically/ do it with OBS services or emulate it with forcelocal -- and most packages actually store the archive in OBS rather than pulling it at build time -- but it would be nice to do both).
[1]: https://github.com/opencontainers/runc/blob/v1.4.0-rc.1/runc... [2]: https://github.com/opencontainers/runc/blob/v1.4.0-rc.1/scri... [3]: https://github.com/opencontainers/runc/blob/v1.4.0-rc.1/scri... [4]: https://build.opensuse.org/projects/openSUSE:Factory/package...
I think that one hole is that even if you require signatures, not all authors will adhere to best practices and some will still be compromised.
Also, five-dollar-wrench attacks remain feasible, although I’m uncertain if we’ve seen them in the real world.
The main issue I have is that these ecosystems add so many other layers of trust you need to have that are unnecessary (trust that source forges like GitHub won't ever be compromised, trust that the access control of said source forges won't ever be compromised, trust that the per-language package repos won't ever be compromised, trust that API keys won't be leaked without being discovered quickly, etc etc).
Why is local 2FA unsustainable?! The real problem here is automated publishing workflows. The overwhelming majority of NPM packages do not publish often enough or have complicated enough release steps to justify tokens with the power to publish without human intervention.
What is so fucking difficult about running `npm publish` manually with 2FA? If maintainers are unwilling to do this for their packages, they should reconsider the number of packages they maintain.
I can look into that.
https://github.com/danielroe/provenance-action
- [GitHub - safedep/vet: Protect against malicious open source packages](https://github.com/safedep/vet)
- [GitHub - AikidoSec/safe-chain](https://github.com/AikidoSec/safe-chain)
- npm audit
I freaking HATE tokens. I hate them.
There should be a better way to do authentication than a glorified static password.
An example of how to do it correctly: Github as a token provider for AWS: https://aws.amazon.com/blogs/security/use-iam-roles-to-conne... But this is an exception, rather than a rule.
In the case of this worm, the OIDC flow wouldn’t even help. The GitHub workflow was compromised. If the workflow was using an OIDC credential like this to publish to npm, the only difference would be the npm publish command wouldn’t use any credential because the GitHub workflow would inject some temporary identity into the environment. But the root problem would remain: an untrusted user shouldn’t be able to execute a workflow with secret parameters. Maybe OIDC would limit the impact to be more fine-grained, but so would changing the token permissions.
If I control the issuing and governance of these short-lived secrets, they very much help against many attacks. Go ahead and extract an upload token for one project which lives for 60 seconds, be my guest. Once I lose control how these tokens are created, most of these advantages go away - you can just create a token every minute, for any project this infrastructure might be responsible for.
If I maintain control about my pipeline definition, I can again do a lot of work to limit damage. For example, if I am in control, I can make sure the stages running untrusted codes have as little access to secrets as possible, and possibly isolate them in bubblewrap, VMs, ..., minimize the code with access to publishing rights. Once I lose control about the pipeline structure, all that goes away. Just add a build step to push all information and secrets to mastodon in individual toots, yey.
To me, this has very much raised questions about keeping pipeline definitions and code in one repository. Or at least, to keep a publishing/release process in there. I don't have a simple solution there, especially for OSS software with little infrastructure - it's not an easy topic. But with these supply chain attacks coming hot and fast every 2 weeks, it's something to think about.
It would have made little difference if the environment variable was NPM_WEBIDENTITY instead of NPM_TOKEN. The workflow was still compromised.
Speaking knowingly reductionistically and with an indeterminate amount of sarcasm, one of the hardest problems in security is how to know something without knowing something. The first "knowing something" is being able to convince a security system to let you do something, and the second is the kind that an attacker can steal.
We do a lot of work trying to separate those two but it's a really, really hard problem, right down at its very deepest core.
I know I was amused 5-10 years ago as we went through a lot of gymnastics. "We have an SSH password here that we use to log in to this system over there and run this process." "That's not secure, because an attacker can get the password. Move that to an SSH key." "That's not secure, an attacker can get the key. Move the key into this secret manager." "That's not secure, an attacker can get into the secret manager. Move it to this 2FA system." "That's not secure, an attacker can get the 2FA token material, move it to...."
There are improvements you can make; if nothing else a well-done 2FA system means an attacker has to compromise 2 systems to get in, and if they are non-correlated that's a legit step up. But I don't think there's a full solution to "the attacker could" in the end. Just improvements.
there are also interesting things you can do in combination with branch protection rulesets and limiting which tags/workflows can generate tokens with specific permissions
You won't be able to exfiltrate a token that allows you to publish an NPM package outside of a workflow, the infection has to happen during a build on GH.
Solutions like generating them live with a short lifetime, using solutions like oauth w/ proper scopes, biscuits that limit what they can do in detail, etc, all exist and are rarely used.
One of the advantages of Trusted Publishing [0] is that we no longer need long-lived tokens with publish rights. Instead, tokens are generated on the CI VM and are valid for only 15 minutes.
This has already been implemented in several ecosystems (PyPI, npm, Cargo, Homebrew), and I encourage everyone to use it, it actually makes publishing a bit _easier_.
More importantly, if the documentation around this still feels unclear, don’t hesitate to ask for help. Ecosystem maintainers are usually eager to see wider adoption of this feature.
[0] https://docs.pypi.org/trusted-publishers/
Guess I know what I’ll be doing this weekend.
I've got no problem with doing an MFA prompt to confirm publish by a CI workflow - but last I looked this was a convoluted process of opening a https tunnel out (using a third party solution) such that you could provide the code.
I'd love to see either npm or GitHub provide an easy, out the box way, for me to provide/confirm a code during CI.
I think the right way to approach this is to unbundle uploading the packages & publishing packages so that they're available to end-users.
CI systems should be able to build & upload packages in a fully automated manner.
Publishing the uploaded packages should require a human to log into npmjs's website & manually publish the package and go through MFA.
I also think it makes sense for GitHub to implement the ability to mark a workflow as sensitive and requiring "sudo mode" (MFA prompt) to run. It's not miles away from what they already do around requiring maintainer approval to run workflows on PRs.
Ideally both of these would exist, as not every npm package is published via GitHub actions (or any CI system), and not every GitHub workflow taking a sensitive action is publishing an npm package.
This is how Go works: you import by URL, e.g. "example.com/whatever/pkgname", which is presumed to be a VCS repo (git, mercurial, subversion, etc.) Versioning is done by VCS tags and branches. You "publish" by adding a tag.
While VCS repos can and have been compromised, this removes an entire attack surface from the equation. If you read every commit or a diff between two tags, then you've seen it all. No need to also diff the .tar.gz packages. I believe this would have prevented this entire incident, and I believe also the one from a few weeks ago (AFAIK that also only relied on compromised npm accounts, and not VCS?)
The main downside is that moving a repo is a bit harder, since the import path will change from "host1.com/pkgname" to "otherhost.com/pkgname", or "github.com/oneuser/repo" to "github.com/otheruser/repo". Arguably, this is a feature – opinions are divided.
Other than that, I can't really think of any advantages a "publish package"-step adds? Maybe I'm missing something? But to me it seems like a relic from the old "upload tar archive to FTP" days before VCS became ubiquitous (or nigh-ubiquitous anyway).
However, this isn’t a problem specific to JavaScript – for example, Python has a much richer standard library and we still see the same types of attacks on PyPI. The entire open source world has been built on an concept of trust which was arguably always more optimistic than realistic, and everyone is pivoting – especially after cryptocurrency’s inherent insecurity created enough of a profit margin to incentivize serious attacks.
Similarly, newer versions of Go change the compiler–which to be first is a good thing–so even if I start with the same source in Git I might not have the same compiled bytes in the result.
Again, mone of this is a bad thing: it just means that I want to compile binaries and ship those so they don’t unexpectedly change in the future and my CI pipeline doesn’t need to have a full Go build stage when all I want is to use Crane to do something with a container.
the other day i was wondering why the terraform aws provider binary was now around 800MB compiled https://github.com/hashicorp/terraform-provider-aws/issues/3...
I've encountered a lot of backlash, but is it really that alien concept these days? CI/CD is cool, but between this and recent CF drama it seems we have a pretty solid evidence it can lead to a serious problems.
I worked at a BigBank once where deployments to production required at least five people present at a time and a lot of theatrics, but at least we knew what we were deploying.
I have yet to see any evidence that fancy CI/CD systems are better than good old fashioned tarballs and detached signatures. Bonus points for distribution packaging systems where they add an additional layer of review and separate validation of releases. People seem to gloss over that fact the "stodgy old-fashioned" rigamarole of Debian is part of the reason why the entire internet didn't pwned by the xz attack.
At the very least you should require a human to sign the blobs before the release is actually published. (This isn't always enough if the attacker can add themselves to the maintainers list and sign with their own key, which is why the distribution packaging systems where they maintain their own trusted copy of upstream keyrings is far more preferable.)
If your threat model boils down to "if my GitHub account gets attacked or even a single API key is leaked all of my users are fucked" then you really need to take a long look at a mirror and ask yourself if that is reasonable.
And then there's other non-sensical proposals like spelunking deep into projects some which could be over a decade old and just rip out all the dependencies until there's nothing but a standard library is left. Look, I'm all for a better std lib, I think reducing the number of dependencies we have is good. But just saying "you should reduce dependencies" will do nothing concrete to fix the problem which already exists, because it's much easier said than done.
So either tens of thousands or hundreds of thousands of developers stop using npm, and everyone refactors their projects to add more code and strip dependencies, or npm starts enforcing things like 2FA and OIDC for package developers with over X number of weekly downloads, and blocks publishing for those that don't follow the new security rules. I think it's clear which solution is more practical to implement. The only other option is for npm to completely lose its reputation and then we wind up with XKCD 927 again.