Skip to content(if available)orjump to list(if available)

Tj-actions/changed-files GitHub Action Compromised – used by over 23K repos

rarkins

Hi, Renovate author/maintainer here.

The affected repo has now been taken down, so I am writing this partly from memory, but I believe the scenario is:

1. An attacker had write access to the tj-actions/changed-files repo

2. The attacker chose to spoof a Renovate commit, in fact they spoofed the most recent commit in the same repo, which came from Renovate

3. Important: this spoofing of commits wasn't done to "trick" a maintainer into accepting any PR, instead it was just to obfuscate it a little. It was an orphan commit and not on top of main or any other branch

4. As you'd expect, the commit showed up as Unverified, although if we're being realistic, most people don't look at that or enforce signed commits only (the real bot signs its commits)

5. Kind of unrelated, but the "real" Renovate Bot - just like Dependabot presumably - then started proposing PRs to update the action, like it does any other outdated dependency

6. Some people had automerging of such updates enabled, but this is not Renovate's default behavior. Even without automerging, an action like this might be able to achieve its aim only with a PR, if it's run as part of PR builds

7. This incident has reminded that many people mistakenly assume that git tags are immutable, especially if they are in semver format. Although it's rare for such tags to be changed, they are not immutable by design

srvaroa

>7. This incident has reminded that many people mistakenly assume that git tags are immutable, especially if they are in semver format. Although it's rare for such tags to be changed, they are not immutable by design

IME, this will be more "learned" than "reminded". Many many people set up pipelines to build artefacts based on tags (e.g. a common practise being "on tag with some pattern, then build artefact:$tag") and are just surprised if you call out the flaws.

It's one of many practises adopted because everyone does it but without basic awareness of the tradeoffs. Semver is another similar case of inherited practise, where surprisingly many people seem to believe that labelling software with a particular string magically translates into hard guarantees about its behaviour.

junon

I theorized about this vulnerability a while back when I noticed new commits didn't disable automerging. This is an insane default from GH.

EDIT: seems GitHub has finally noticed (or started to care); just went to test this and auto merge has been seemingly disabled sitewide. Even though the setting is enabled, no option to automerge PRs shows up.

Seems I was right to worry!

EDIT2: We just tested this on GitLab's CI since they also have an auto-merge function and it appears they've done things correctly. Auto-merge enablement is only valid for the commit for which it was enabled; new pushes disable auto-merge. Much more sensible and secure.

WhyNotHugo

GitLab has had this behaviour (disable auto-merge when new commits are pushed) since long before GitHub even had auto-merge.

It’s such an obvious attack vector, I’m pretty sure I tested GitLab soon after the feature initially rolled out.

nine_k

Tags can be signed, and the signature can be verified. It's about as easy as signing / verifying commits. One can even make signing tags as the default option when creating tags.

This won't help in this case though, because a legitimate bot was tricked into working with a rogue commit; a tricked bot could as well sign a tag with a legitimate key.

"Immutable tags" of course exist, they are commit hashes, but they are uninformative :(

klysm

How else should we do it?

sunnybeetroot

By commit hash

diggan

> 6. Some people had automerging of such updates enabled, but this is not Renovate's default behavior. Even without automerging, an action like this might be able to achieve its aim only with a PR, if it's run as part of PR builds

I'm not sure how this could exploited by just making a PR, unless you for some reason have secrets enabled for builds by unknown contributors, which obviously would be a mistake. Usually, only builds using secrets only run on certain branches which has a known contributor approving the code before it gets there.

> people mistakenly assume that git tags are immutable

If you're distributing a library on GitHub used by many other people/projects, then you really need to setup `protected branches` and `protected tags`, where you can prevent changes somewhat.

semiquaver

  > I'm not sure how this could exploited by just making a PR, unless you for some reason have secrets enabled for builds by unknown contributors
In this context the renovate bot would be making the PR to a repo it had been installed on, making it a trusted contributor able to trigger CI builds on its PRs.

jonenst

Neither Branch Protection nor the newer Rulesets allow to protect secrets from someone with push acces to the repo. From what I understand, only environment secrets provide this feature (and have the drawback that you can't share them among multiple repos in the same org without copying them everywhere, although you can script the copying with the github api)

mlor

Thanks for taking the time to comment. Not that it wasn't there before this, but this incident highlights a lot to take into consideration with respect to securing one's supply chain going forward.

afitnerd

Thanks for this writeup! It seems like #1 was the real weakness. Have you identified how the attacker was able to get write access to tj-actions/changed-files? Did this discovery result in any changes to how people can contribute to the project?

3np

So I follow steps 1-4 but don't see what triggered step 5, if they indeed just did an orphan commit outside of releases or main branch?

mubou

In recent years, it's started to feel like you can't trust third-party dependencies and extensions at all anymore. I no longer install npm packages that have more than a few transitive dependencies, and I've started to refrain from installing vscode or chrome extensions altogether.

Time and time again, they either get hijacked and malicious code added, or the dev themselves suddenly decides to betray everyone's trust and inject malicious code (see: Moq), or they sell out to some company that changes the license to one where you have to pay hundreds of dollars to keep using it (e.g. the recent FluentAssertions debacle), or one of those happens to any of the packages' hundreds of dependencies.

Just take a look at eslint's dependency tree: https://npmgraph.js.org/?q=eslint

Can you really say you trust all of these?

ashishb

> Can you really say you trust all of these?

We need better capabilities. E.g. when I run `fd`, `rg` or similar such tool, why should it have Internet access?

IMHO, just eliminating Internet access for all tools (e.g. in a power mode), might fix this.

The second problem is that we have merged CI and CD. The production/release tokens should ideally not be on the same system as the ones doing regular CI. More users need access to CI (especially in the public case) than CD. For example, a similar one from a few months back https://blog.yossarian.net/2024/12/06/zizmor-ultralytics-inj...

nextaccountic

> We need better capabilities. E.g. when I run `fd`, `rg` or similar such tool, why should it have Internet access?

Yeah!! We really need to auto sandbox everything by default, like mobile OSes. Or the web.

People browse the web (well, except Richard Stallman) all the time, and run tons of wildly untrusted code, many of them malicious. And apart from zero days here and there, people don't pay much attention to it, and will happily enter any random website in the same machine they also store sensitive data.

At the same time, when I open a random project from Github on VSCode, it asks whether the project is "trusted". If not, it doesn't run the majority of features like LSP server. And why not? Because the OS doesn't sandbox stuff by default. It's maddening.

redserk

I’ve been doing all of my dev work in a virtual machine as a way to clamp things down. I’ve even started using a browser in a VM as a primary browser.

Computers are fast enough where the overhead doesn’t feel like it’s there for what I do.

For development, I think Vagrant should make a comeback as one of the first things to setup in a repo/group of repos.

bombcar

https://www.qubes-os.org/ is the extension of this.

hypeatei

OpenBSDs pledge[0] system call is aimed at helping with this. Although, it's more of a defense-in-depth measure on the maintainers part and not the user.

> The pledge() system call forces the current process into a restricted-service operating mode. A few subsets are available, roughly described as computation, memory management, read-write operations on file descriptors, opening of files, networking (and notably separate, DNS resolution). In general, these modes were selected by studying the operation of many programs using libc and other such interfaces, and setting promises or execpromises.

[0]: https://man.openbsd.org/pledge.2

yencabulator

Pledge is for self-isolating, it helps with mistakes but not against intentional supply chain attacks.

mnahkies

I've found firejail to be a useful tool for this (https://github.com/netblue30/firejail), and additionally use opensnitch (https://github.com/evilsocket/opensnitch) to monitor for unexpected network requests.

For CI/CD using something like ArgoCD let's you avoid giving CI direct access to prod - it still needs write access to a git repo, and ideally some read access to Argo to check if deployment succeeded but it limits the surface area.

varunsharma07

Great points! Harden-Runner (https://github.com/step-security/harden-runner) is similar to Firejail and OpenSnitch but purpose-built for CI/CD context. Harden-Runner detected this compromise due to an anomalous outbound network request to gist.githubusercontent.com.

Interestingly, Firejail itself uses Harden-Runner in its GitHub Actions workflows! https://github.com/search?q=repo%3Anetblue30%2Ffirejail%20ha...

homebrewer

bubblewrap is a safer alternative to firejail because it does not use setuid to do its job, and it is used by flatpak (so hopefully has more eyes on it, but I have no idea).

https://wiki.archlinux.org/title/Bubblewrap

You do have to assemble isolation scripts by hand though, it's pretty low level. Here is a decent comment which closely aligns to what I'm using to isolate npm/pnpm/yarn/etc, I see no need to repeat it:

https://news.ycombinator.com/item?id=43369927

ptx

FreeBSD has Capsicum [0] for this. Once a process enters capability mode, it can't do anything except by using already opened file descriptors. It can't spawn subprocesses, connect to the network, load kernel modules or anything else.

To help with things that can't be done in the sandbox, e.g. DNS lookups and opening new files, it provides the libcasper library which implements them using helper processes.

Not all utilities are sandboxed, but some are and hopefully more will be.

Linux recently added Landlock [1] which seems sort of similar, although it has rulesets and doesn't seem to block everything by default, as far as I can tell from quickly skimming the docs.

[0] https://wiki.freebsd.org/Capsicum

[1] https://docs.kernel.org/userspace-api/landlock.html

theamk

I don't think it would help in this case, when the entire process can be replaced with malicious version. It just won't make the Capscium call.

What you really want is something external and easily inspectable, such as systemd per-service security rules, or flatpak sandboxing. Not sure if FreeBSD has somethingike this.

CamJN

You also need to block write access, so they can’t encrypt all your files with an embedded public key. And read access so they can’t use a timing side channel to read a sensitive file and pass that info to another process with internet privileges to report the secret info back to the bad guy. You get the picture, I’m sure.

pdimitar

I get the picture, yes, namely that probably 99% of project dependencies don't need I/O capabilities at all.

And even if they do, they should be controlled in a granular manner i.e. "package org.ourapp.net.aws can only do network and it can only ping *.aws.com".

Having finer-grained security model that is enforced at a kernel level (and is non-circumventable barring rootkits) is like 20 years overdue at this point.

Every single big org is dragging their feet.

ashishb

> You also need to block write access, so they can’t encrypt all your files with an embedded public key. And read access so they can’t use a timing side channel to read a sensitive file and pass that info to another process with internet privileges to report the secret info back to the bad guy. You get the picture, I’m sure.

Indeed.

One can think of a few broad capabilities that will drastically reduce the attack surface.

1. Read-only access vs read-write 2. Access to only current directory and its sub-directories 3. Configurable Internet access

Docker mostly gets it right. I wish there was an easy way to run commands under Docker.

E.g.

If I am running `fd`

1. Mount current read-only directory to Docker without Internet access (and without access to local network or other processes) 2. Run `fd` 3. Print the results 4. Destroy the container

h4ck_th3_pl4n3t

But that's what firejail and docker/podman are for. I never run any build pipeline on my host system, and neither should you. Build containers are pretty good for these kind of mitigations of security risks.

mschuster91

> We need better capabilities.

I'd love to say "just use Kubernetes and run Nexus as a service inside" but unfortunately Network Policies are seriously limited [1]...

[1] https://kubernetes.io/docs/concepts/services-networking/netw...

from-nibly

This is the death of fun. Like when you had to use SSL for buying things online.

Adding SSL was not bad, don't get me wrong. It's good that it's the default now. However. At one point it was sorta risky, and then it became required.

Like when your city becomes crime ridden enough that you have to lock your car when you go into the grocery store. Yeah you probably should have been locking it the whole time. what would it have really cost? But now you have to, because if you don't your car gets jacked. And that's not a great feeling.

aorloff

Just you wait. Here in America when your city becomes crime ridden enough you start leaving it unlocked again.

mubou

Used to live near San Francisco, and had a lot of coworkers say they intentionally leave their windows down when parking in SF so that burglars don't break the glass to steal something!

asveikau

Crime is lower than the 80s and 90s. It has been declining since 2023.

nozzlegear

On the other extreme, I can (and do) leave my keys inside my running car while I shop for groceries!

asveikau

In the era of the key fob it's pretty automatic to lock the car every time. Some cars even literally do it for you. I hardly think of this, let alone get not great feelings about it.

prepend

I liked living in a city where I could leave my doors unlocked and windows down. It was less to worry about.

usef-

Yes. Same with browser plugins. I've heard multiple free-plugin authors say they're receiving regular offers to purchase their projects. I'm sure some must take up the offer.

ronjouch

For an example of a scary list of such offers, see https://github.com/extesy/hoverzoom/discussions/670

mubou

This is why I fork the extensions I use, with the exception of uBlock. Basically just copy the extension folder, if I can't find it on GitHub. That way I can audit the code and not have to worry about an auto-update sneaking in something nefarious. I've had two extensions in the past suddenly start asking for permissions they definitely did not need, and I suspect this is why.

Btw, here's a site where you can inspect an extension's source code before you install it: https://robwu.nl/crxviewer/

_boffin_

do you know of any other ones like this that post their offers?

remram

This is cool but useless because they redacted all the company names. The opposite of a name and shame, because no name and no shame.

Gigachad

I have long since stopped using any extension that doesn’t belong to an actual company (password managers for example). Even if they aren’t malware when you installed them, they will be after they get sold.

stockhorn

A bit off topic, but how is the bitwarden browser extension protected against supply-chain attacks (npm dependencies)?

fluidcruft

Actual companies also get sold and churned into shit. See LastPass for example.

from-nibly

I got an outreach for an extension I made as a joke. It had like maybe 5000 downloads ever.

mh-

> eslint's dependency tree

And if you turn on devDependencies (top right), it goes from 85 to 1263.

Terr_

I'd also emphasize out that there's nothing safe about it being "only dev", given how many attacks use employee computers (non-prod) as a springboard elsewhere.

XorNot

The original .NET (and I think Java?) had an idea in them of basically library level capability permissions.

That sort of idea seems increasingly like what we need because reputation based systems can be gamed too easily: i.e. there's no reason an action like this ever needed network access.

mdaniel

It was only recently removed in Java and there was a related concept (adopted from OSGi) designed to only export certain symbols -- not for security but for managing the surface area that a library vendor had to support

But I mentioned both of those things because [IMHO] they both fell prey to the same "humanity bug": specifying permissions for anything (source code, cloud security, databases, Kubernetes, ...) is a lot of trial and error, whereas {Effect: Allow, Action: ["*:*"]} always works and so they just drop a "TODO: tighten permissions" and go on to the next Jira

I had high hopes for the AWS feature "Make me an IAM Policy based on actual CloudTrail events" but it talks a bigger game than it walks

scrapcode

Are there examples of these types of actions in other circles outside of the .NET ecosystem? I knew about the FluentAssertions ordeal, but the Moq thing was news to me. I guess I've just missed it all.

do_not_redeem

node-ipc is a recent example from the Node ecosystem. The author released an update with some code that made a request to a geolocation webservice to decide whether to wipe the local filesystem.

mubou

Yeesh. Found an article for anyone interested: https://snyk.io/blog/peacenotwar-malicious-npm-node-ipc-pack...

I like this comment from u/mailto_devnull (https://www.reddit.com/r/node/comments/tg451e/do_not_use_nod...):

  Where do I stand on the war? I stand with Ukraine.
  Where do I stand on software supply chain issues? I stand with not fucking around with the software supply chain.

sanex

Missed them too. Always was annoyed by FluentAssertions anyway, some contractor added it to a project that we took over couldn't see the value add.

puffybuf

Stealing crypto is so lucrative. So there is a huge 'market' for this stuff now that wasn't there before. Security is more important now than ever. I started sandboxing Emacs and python because I can't trust all the packages.

semi-extrinsic

What do you use for sandboxing?

ozim

You should never have trusted blindly in third-party dependencies in the first place.

Abnormal behavior was to trust by default.

kurmiashish

Disclaimer: I am a co-founder of StepSecurity.

StepSecurity Harden-Runner detected this security incident by continuously monitoring outbound network calls from GitHub Actions workflows and generating a baseline of expected behaviors. When the compromised tj-actions/changed-files Action was executed, Harden-Runner flagged it due to an unexpected endpoint appearing in the network traffic—an anomaly that deviated from the established baseline. You can checkout the project here: https://github.com/step-security/harden-runner

cyrnel

The advertising in this article is making it actively difficult to figure out how to remediate this issue. The "recovery steps" section just says "start our 14 day free trial".

The security industry tolerates self-promotion only to the extent that the threat research benefits everyone.

kurmiashish

Thank you, cyrnel, for the feedback! We are trying our best to help serve the community. Now, we have separate recovery steps for general users and our enterprise customers.

cyrnel

Thanks for the edit! In "incident response mode" every moment counts!

shawabawa3

A simpler method to detect this would be to store GitHub action tag hashes and freeze an action if any tag is changed

harrisi

It's always been shocking to me that the way people run CI/CD is just listing a random repository on GitHub. I know they're auditable and you pin versions, but it's crazy to me that the recommended way to ssh to a server is to just give a random package from a random GitHub user your ssh keys, for example.

This is especially problematic with the rise of LLMs, I think. It's the kind of common task which is annoying enough, unique enough, and important enough that I'm sure there are a ton of GitHub actions that are generated from "I need to build and deploy this project from GitHub actions to production". I know, and do, know to manually run important things in actions related to ssh, keys, etc., but not everyone does.

remram

People don't pin versions. Referencing a tag is not pinning a version, those can be updated, and they are even with the official actions from GitHub.

jakub_g

I think a big part of the problem is the way one typically "installs" a GH action: by copy-pasting something from README of the action.

Let's have a look at a random official GH provided action:

https://github.com/actions/checkout

It lists the following snippet:

`uses: actions/checkout@v4`

Almost everyone will just copy paste this snippet and call it a day. Most people don't think twice that v4 is a movable target that can be compromised.

In case of npm/yarn deps, one would often do the same, and copy paste `yarn install foobar`, but then when installing, npm/yarn would create a lockfile and pin the version. Whereas there's no "installer" CLI for GH actions that would pin the version for you, you just copy-paste and git push.

To make things better, ideally, the owners of actions would update the workflows which release a new version of the GH action, to make it update README snippet with the sha256 of the most recent release, so that it looks like

`uses: actions/checkout@abcdef9876543210` # v4.5.6

Since GitHub doesn't promote good defaults, it's not surprising that third-party maintainers do the same.

xign

I mean, I think there's a difference between trusting GitHub and trusting third parties. If I can't trust GitHub, then there's absolutely no point in hosting on GitHub or trusting anything in GitHub Actions to begin with.

But yes I do think using tags is problematic. I think for one, GitHub should ban re-tagging. I can't think of a good reason for a maintainer to re-publish a tag to another commit without malicious intent. Otherwise they should provide a syntax to pin to both a tag and a commit, something like this:

`uses: actions/checkout@v4.5.6@abcdef9876543210`

The action should only work if both conditions are satisfied. This way you can still gain semantics version info (so things like dependabot can work to notify an update) but the commit is still pinned.

---

I do have to say though, these are all just band-aids on top of the actual issue. If you are actually using a dependency that is compromised, someone is going to get screwed. Are you really going to read through the commit and the source code to scan for suspicious stuff? I guess if someone else got screwed before you did they may report it, but it's still fundamentally an issue here. The simple answer is "don't use untrustworthy repositories" but that is hard to guarantee. Only real solution is to use as few dependencies as possible.

0rzech

Some people do actually pin versions, like me. For instance:

  - uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2.7.7
or

  - uses: subosito/flutter-action@f2c4f6686ca8e8d6e6d0f28410eeef506ed66aff # v2.18.0
It's a bit more manual work, but lepiej dmuchać na zimne (lit. it is better to blow on something cold), as the Polish proverb says.

brokenpip3

>It's a bit more manual work

after this incident, I started pinning all my github workflows with hashes, like other folks here I guess :D But I quickly got tired of doing it manually so I put together this [0] quick and dirty script to handle it for me. It just updates all workflow files in a repo and can be also used as a pre-commit hook to catch any unpinned steps in the future. It’s nothing fancy (leveraging ls-remote), but it’s saved me some time, so I figured I’d share in case it helps someone else :)

[0] https://github.com/brokenpip3/pre-commit-hooks?tab=readme-ov...

matsemann

You would still be exposed if you had renovate or dependabot make a PR where they update the hash for you, though. Here's a PR we got automatically created the other day:

-uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3 +uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3

and this PR gets run with privileges since it's from a user with write permissions.

harrisi

Aren't GitHub action "packages" designate by a single major version? Something like checkout@v4, for example. I thought that that designated a single release as v4 which will not be updated?

I'm quite possibly wrong, since I try to avoid them as much as I can, but I mean.. wow I hope I'm not.

remram

No the "v4" tag gets updated from v4.1 to v4.2 etc as those minor versions are released. They are branches, functionally.

sestep

The crazier part is, people typically don't even pin versions! It's possible to list a commit hash, but usually people just use a tag or branch name, and those can easily be changed (and often are, e.g. `v3` being updated from `v3.5.1` to `v3.5.2`).

nextts

Fuck. Insecure defaults again. I argue that a version specifier should be only a hash. Nothing else is acceptable. Forget semantic versions. (Have some other method for determining upgrade compatibility you do out of band. You need to security audit every upgrade anyway). Process: old hash, new hash, diff code, security audit, compatibility audit (semver can be metadata), run tests, upgrade to new hash.

harrisi

You and someone else pointed this out. I only use GitHub-org actions, and I just thought that surely there would be a "one version to rule them all" type rule.. how else can you audit things?

I've never seen anything recommending specifying a specific commit hash or anything for GitHub actions. It's always just v1, v2, etc.

mcpherrinm

OpenSSF scorecard flags dependencies (including GitHub actions) which aren’t pinned by hash

https://scorecard.dev/

https://github.com/ossf/scorecard/blob/main/docs/checks.md#p...

tommasoamici

Having to use actions for ssh/rsync always rubbed me the wrong way. I’ve recently taken the time to remove those in favor of using the commands directly (which is fairly straightforward, but a bit awkward).

I think it’s a failure of GitHub Actions that these third party actions are so widespread. If you search “GitHub actions how to ssh” the first result should be a page in the official documentation, instead you’ll find tens of examples using third party actions.

kalaksi

So much this. I recently looked into using GitHub Actions but ended up using GitLab instead since it had official tools and good docs for my needs. My needs are simple. Even just little scripting would be better than having to use and audit some 3rd party repo with a lot more code and deps.

And if you're new, and the repo aptly named, you may not realize that the action is just some random repo

12_throw_away

> It's always been shocking to me that the way people run CI/CD is just listing a random repository on GitHub.

Right? My mental model of CI has always been "an automated sequence of commands in a well-defined environment". More or less an orchestrated series of bash scripts with extra sugar for reproducibility and parallelism.

Turning these into a web of vendor-locked, black-box "Actions" that someone else controls ... I dunno, it feels like a very pale imitation of the actual value of CI

Sytten

I am surprised nobody here mentionned immutable github actions that are coming [1]. Been waiting for them since the issue was open in 2022. This would have significantly reduce impact and hopefully github will get it over the finish line.

I always fork my actions or at least use a commit hash.

[1] https://github.com/features/preview/immutable-actions

ricardobeat

I thought actions were already immutable and published to a registry, not fetched directly from their repo. TIL.

Go also uses tags for module versioning, and while go.mod or package-lock.json stop this attack from reaching existing consumers, allowing remapping of all versions to the compromised one still expands the impact surface a lot. GitHub should offer a “immutable tags” setting for repos like these.

null

[deleted]

ebfe1

Doing a bit of investigation with github_events in clickhouse, it is quite clear that the accounts used to perform the attack was "2ft2dKo28UazTZ", "mmvojwip" also seems suspicious:

https://play.clickhouse.com/play?user=play#c2VsZWN0ICogZnJvb...

Actions taken by the threat actor at the time can be seen here:

https://play.clickhouse.com/play?user=play#c2VsZWN0ICogZnJvb...

ebfe1

It seems i forgot to cater for the quota applied to free "play" user in ClickHouse in my previous query... In fact, the threat actor did a lot more... this should give a better list of actions that was performed - Clearly showed he was testing his payload:

https://play.clickhouse.com/play?user=play#c2VsZWN0ICogZnJvb...

DavyJone

Nice find. Its a bit strange that the PRs listed there, are not present at all in the coinbase repo. Seems like the attack was directed there, but I also did not hear anything from Coinbase on this.

eg. Target their NPM and PYPI tokens, so they can push compromised packages.

ebfe1

I wonder if they forked it to "experiment" with the workflow coinbase has and doesn't actually make any pull request toward them, perhaps to validate their hypothesis/attack. with that said, coinbase pulled the workflow that used tj-actions/changed-files immediately around this time so hopefully no harm was done https://github.com/coinbase/agentkit/pull/570/files

ebfe1

Note that these account seems to be deleted now - 2ft2dKo28UazTZ clearly did more than just changed-files and also seem to target coinbase/agentkit as well (Actually .. they might be targeted by the threat actor)

rognjen

That is a really very likely scenario.

The attacker was trying to compromise agentkit and found changed-files used in the repo so looked around. Found that it was using a bot with a PAT to release.

Totally possible the bot account had a weak password, and the maintainer said it didn't have 2FA.

They got the release bot PAT so they tried possibly quite an obvious vector that. They didn't need anything sophisticated or to exfil the credentials because agentkit is public.

It just so happened that it was detected before agentkit updated dependencies.

It's possible that with if thye had checked the dependabot config they could've timed it a bit better so that it's picked up in agentkit before being detected.

edit: Although, I don't think PATs are visible after they're generated?

dan_manges

GitHub Actions should use a lockfile for dependencies. Without it, compromised Actions propagate instantly. While it'd still be an issue even with locking, it would slow down the rollout and reduce the impact.

Semver notation rather than branches or tags is a great solution to this problem. Specify the version that want, let the package manager resolve it, and then periodically update all of your packages. It would also improve build stability.

nextts

Also don't het GH actions to do anything other than build and upload artifacts somewhere. Ideally a write only role. Network level security too no open internet.

Use a seperate system for deployments. That system must be hygienic.

This isn't foolproof but would make secrets dumping not too useful. Obviously an attack could still inject crap into your artefact. But you have more time and they need to target you. A general purpose exploit probably won't hurt as much.

mixologic

All the version tags got relabled to point to a compromised hash. Semver does nothing to help with this.

your build should always use hashes and not version tags of GHA's

cmckn

I always use commit hashes for action versions. Dependabot handles it, it’s a no brainer.

Terr_

> commit hashes

There is some latent concern that most git installations use SHA-1 hashes, as opposed to SHA-256. [0]

Also the trick of creating a branch that happens to be named the same as a revision, which then takes precedence for certain commands.

[0] https://git-scm.com/docs/hash-function-transition

password4321

creating a branch that happens to be named the same as a revision, which then takes precedence for certain commands

TIL; yikes! (and thanks)

null

[deleted]

mceachen

GitHub actions supports version numbers, version ranges, and even commit hashes.

werrett

Only commit hashes are safe. In this case the bad actor changed all of the version tags to point to their malicious commit. See https://github.com/tj-actions/changed-files/tags

All the tags point to commit `^0e58ed8` https://github.com/tj-actions/changed-files/commit/0e58ed867...

frenchtoast8

The version numbers aren't immutable, so an attacker can just update the versions to point to the compromised code, which is what happened here. Commit hashes are a great idea, but you still need to be careful: lots of people use bots like Renovate to update your pinned hashes whenever a new version is published, which runs into the same problem.

marsovo

I don't think that's exactly what happened here: the compromise created new tags but generally the tag consumption relies on semantic versioning

In other words: you specify version 44, the attacker creates 44.1, you're still hosed.

jasonthorsness

Since they edited old tags here … maybe GitHub should have some kind of security setting a repo owner can make that locks-down things like old tags so after a certain time they can't be changed.

CaliforniaKarl

In your GitHub Actions YAML, instead of referencing a specific tag, you can reference a specific commit. So, instead of …

    uses: actions/checkout@v4
… you can use …

    uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683

OptionOfT

That still doesn't help when the action is a docker action only marked with a tag.

So you need to check the action.yml itself to see if it has a sha256 pinned (in the case it uses Docker).

eddythompson80

You can always just fork it and reference your own fork.

postalrat

Or just write your own.

themgt

This is hilarious, the maven-lockfile project "Lockfiles for Maven. Pin your dependencies. Build with integrity" appears to have auto-merged a PR for the compromised action commit. So the real renovate bot immediately took the exfiltration commit from the fake renovate bot and started auto-merging it into other projects:

https://github.com/chains-project/maven-lockfile/pull/1111

sureIy

The fun part is that they used commits specifically for security, but then add an auto-updater. Might as well use tags.

mdaniel

heh, timing is everything https://github.com/chains-project/maven-lockfile/issues/1085...

> After some cleanup the changed-files (https://github.com/tj-actions/changed-files) action seems to be more work to remove. It would be awesome if it could be added to the allowlist

> Done. Allowed all versions of this action. Should I pin it to one version in the allowlist (won't be convenient if renovate updates this dependency)?

onnimonni

It seems pretty awful that the de-facto way to use GitHub Actions is using git tags which are not immutable. For example to checkout code [1]:

- uses: actions/checkout@v4

Github does advise people to harden their actions by referring to git commit hashes [2] but Github currently only supports SHA-1 as hashing algorithm. Creating collisions with this hashing algo will be more and more affordable and I'm afraid that we will see attacks using the hash collisions during my lifetime.

I wish that they will add support for SHA-256 soon and wrote product feedback regarding it here: https://github.com/orgs/community/discussions/154056

If this resonates with you please go and give it a thumbs up :)

[1]: https://github.com/actions/checkout?tab=readme-ov-file#usage

[2]: https://docs.github.com/en/actions/security-for-github-actio...

ffsm8

> ... SHA-1 ... Collusions ... will be more and more affordable.

I can put your fears on that account to rest. At current trajectory, that's not gonna happen.

While a collision has been successfully produced, that's a very far milestone away from creating a specific collision with a payload you actually want to deliver with reasonable size so any sanity check such as a multi GB file size wouldnt "accidentally" detect it through timeouts in CI or similar.

This is so far beyond our current technological capabilities and Moore's law hasn't been active for over a decade now. Sure, we've had astounding success in the GPU space, but that's still not even remotely close to the previous trajectory while on Moore's Law.

onnimonni

I wasn't aware of the already existing SHA-1 collision support created by Github. It's very interesting read and AFAIK it seems that using SHA-1 collisions is not possible:

https://github.blog/news-insights/company-news/sha-1-collisi...

Is anyone aware of a git hook I could use to analyse my .github/workflows/*.yml files and replace git tags like "v4" with the current git commit hashes?

I think this would make it much safer to use 3rd party GitHub Actions.

Y_Y

That's the sort of hook you should be able to write yourself pretty quickly. So I threw your comment into o3-mini-high and it gave me a decent-looking solution. Decent but wrong, since it thought "current git commit" referred to the project repo, rather than the referenced dependency.

Anyway here's the gist of a solution without any of the necessary checking that the files actually exist etc.

  #!/bin/sh
  for file in .github/workflows/*.yml; do
    grep -E "uses:[[:space:]]+[A-Za-z0-9._-]+/[A-Za-z0-9._-]+@v[0-9]+" "$file" | while read -r line; do
      repo=$(echo "$line" | sed -E 's/.*uses:[[:space:]]+([A-Za-z0-9._-]+\/[A-Za-z0-9._-]+)@v[0-9]+.*/\1/')
      tag=$(echo "$line" | sed -E 's/.*@((v[0-9]+)).*/\1/')
      commit_hash=$(git ls-remote "https://github.com/$repo.git" "refs/tags/$tag" | awk '{print $1}')
      [ -n "$commit_hash" ] && sed -i.bak -E "s|(uses:[[:space:]]+$repo@)$tag|\1$commit_hash|g" "$file" && git add "$file" && rm -f "$file.bak"
    done
  done
  exit 0

onnimonni

Thanks! Today I learned:

$ git ls-remote "https://github.com/$repo.git" "refs/tags/$tag"

Even though the grep and sed are not very readable this was very useful way to avoid yet another tool!

frenchtoast8

The repository is back online, with this explanation from the developer:

> This attack appears to have been conducted from a PAT token linked to @tj-actions-bot account to which "GitHub is not able to determine how this PAT was compromised."

> Account Security Enhancements

> * The password for the tj-actions-bot account has been updated.

> * Authentication has been upgraded to use a passkey for enhanced security.

> * The tj-actions-bot account role has been updated to ensure it has only the minimum necessary permissions.

> * GitHub proactively revoked the compromised Personal Access Token (PAT) and flagged the organization to prevent further exploitation.

https://github.com/tj-actions/changed-files/issues/2464#issu...

frenchtoast8

Editing to add: the developer has locked further discussion about this. Very concerning as I believe their explanations are raising more questions than they are answering.

First of all, clearly Github can't answer for the developer how their bot's token was compromised, that's something the developer needs to find out. Instead they are repeating this statement like it's out of their hands.

But more concerningly, I don't believe the explanation is supported by the Github history which says the compromised commit was "authored" by Renovate and "pushed" by @jackton1. It's obvious how the first part was spoofed, but the second part is concerning as it indicates the @jackton1 account was compromised not @tj-actions-bot. If I'm missing something please let me know.

thj4742

Check the timestamp on that commit push. It was from today, an hour or two before the repo was restored, not yesterday when the attack happened. The push actor != the committor or even the actual commit author, and there can be multiple push actors if the commit is pushed multiple times by different actors.

He probably just re-pushed the bad commit while trying to figure out how to fix this.

I find it very plausible that the bot token was compromised, not his user account token, as the attack was simply to push over the tags (which is something the automation bot would have access to do, as tag management is one of its functions)

jamesberthoty

Does this seem like a plausible summary?

1. tj-actions-bot PAT spoofs renovatebot commit with malicious code - probably by creating a new unprotected branch, pushing to it spoofing the renovatebot user, then deleting the branch, but we really don't know.

2. Attacker uses PAT to also update release tags, pointing them to the malicious commit, again spoofing renovatebot

3. jackton1 tries to restore older branch, and therefore pushes the commit again. The original commit wouldn't be referenced as pushed in any pull requests

null

[deleted]

oefrha

> https://github.com/tj-actions/changed-files/pull/2460

This kind of auto dependency bump bots are more trouble than their worth. If your app works today, bumping random deps won’t make it work better in any meaningful sense in 95% of cases. With such a small upside, the downside of introducing larger attack surfaces, subtle breakages (despite semver), major breakages, and in the worst cases, compromises (whether it’s a compromised dep, or fake bot commits that people are trained to ignore) just completely outweighs the upside. You’re on the fast lane to compromises by using this kind of crap.

People should really learn from Go’s minimum version selection strategy.

lostmsu

Your app will have unpatched vulnerabilities.

oefrha

As long as you subscribe to security advisories, it’s a lot more likely that new vulnerabilities are introduced than old undiscovered vulnerabilities are accidentally patched. In fact barring rewrites (which usually won’t be picked up by semver-respecting auto bumps anyway) I can hardly think of an example of the latter.

simonw

I've always felt uncomfortable adding other people's actions to my GitHub workflows, and this is exactly the kind of thing I was worried about.

I tend to stick to the official GitHub ones (actions/setup-python etc) plus the https://github.com/pypa/gh-action-pypi-publish one because I trust the maintainers to have good security habits.

3eb7988a1663

That's exactly where I stand, and I feel partially vindicated by this outcome. There are so many useful Github actions made by randos, but I am not adding more unvetted dependencies to my project. I will unhappily copy and paste some useful code into my project rather than relying upon yet another mutable dependency.